<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:dcterms="http://purl.org/dc/terms/"
     xmlns:media="http://search.yahoo.com/mrss/"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:cf="https://www.futureplc.com/rss/content-flags"
>
    <channel>
                    <atom:link href="https://www.tomsguide.com/feeds/tag/speakers" rel="self" type="application/rss+xml" />
                            <title><![CDATA[ Latest from Tom's Guide in Speakers ]]></title>
                <link>https://www.tomsguide.com/audio/speakers</link>
        <description><![CDATA[ All the latest speakers content from the Tom's Guide team ]]></description>
                                    <lastBuildDate>Fri, 04 Sep 2026 14:04:25 +0000</lastBuildDate>
                            <language>en</language>
                                <item>
                                                            <title><![CDATA[ Look no further for stylish sound in compact spaces — I’ve been testing the Kanto UKI speakers and I’m impressed ]]></title>
                                                                                                <dc:content><![CDATA[ <p>If you’re looking for some of the <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html">best computer speakers</a> to upgrade your desk setup, then look no further than the Kanto UKI bookshelf speakers. Though compact they may be, quiet they are not, with a room-filling sound that excellently replicates mids and treble. </p><p>The controls are simple to get to grips with, and the diverse range of connectivity options means you can use them for everything. From wirelessly streaming from your phone to connecting to a turntable, these speakers are incredibly versatile. </p><p>On their own, the speakers struggle to replicate bass and have a narrow soundstage, but these issues can be fixed with additional purchases of a sub and speaker stands. To find out more, you can keep reading this full Kanto UKI review. </p><h2 class="article-body__section" id="section-kanto-uki-review-specs"><span>Kanto UKI review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSTD2S4M" target="_blank" rel="nofollow">$269</a> / <a href="https://www.amazon.co.uk/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSVVT6GL" target="_blank" rel="nofollow">£262</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, USB-C, RCA, 3.5mm out</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>80 Hz - 22kHz</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (per speaker)</strong></p></td><td  ><p>6.5 x 4.4 x 4.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.2 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Onyx, Chalk, Cornflower, Sage, Pumice</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-kanto-uki-review-price-availability"><span>Kanto UKI review: Price & availability</span></h2><p>The Kanto UKI bookshelf speakers are available for <a href="https://www.amazon.com/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSTD2S4M" target="_blank" rel="nofollow">$269 from Amazon U.S</a>. and <a href="https://www.amazon.co.uk/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSVVT6GL" target="_blank" rel="nofollow">£262 from Amazon U.K.</a> The speakers come in five different colors, including Onyx, Chalk, Cornflower, Pumice and Sage, which I tested. </p><p>Kanto also offers the <a href="https://www.tomsguide.com/audio/speakers/kanto-ora">Kanto Ora</a> bookshelf speakers for the higher price of $349. These speakers only come in black, but they offer a wider soundstage and studio sound for music production or mixing. There are also the <a href="https://www.tomsguide.com/audio/kanto-ora4-review">Kanto Ora4</a> for $399, which are the same as the Ora but powered. </p><p>A cheaper way to upgrade your desk setup is the <a href="https://www.tomsguide.com/audio/speakers/jbl-104-bt">JBL 104-BT</a> speakers for $199. They have a wide sound but struggle in the low end. I personally don’t think they look as good as the Kanto speakers, but they do the job just fine. </p><p>If you’re short on space and need to save some space (and money), the <a href="https://www.tomsguide.com/audio/speakers/creative-pebble-pro-review">Creative Pebble Pro </a>speakers cost just $69. The speakers have powerful bass, but I prefer the soundstage of the Kanto UKI speakers. </p><h2 class="article-body__section" id="section-kanto-uki-review-design"><span>Kanto UKI review: Design </span></h2><p>The Kanto UKI speakers are an undeniably stylish duo that has brightened up my desk. I tested the Sage colorway of the speakers, and since my room is decorated with a lot of green furniture, they fit right in. </p><p>Each of the speakers measures 6.5 x 4.4 x 4.3 inches, so they’re very compact. They’re smaller than the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review">Edifier R1280T</a> speakers ($149), which measure 9.2 x 7.7 x 5.7 inches. If you’re short on space, the Kanto speakers are a no-brainer. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CXXy9yLH4MsajcHDK7zLUC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/CXXy9yLH4MsajcHDK7zLUC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>They are made from partially recycled hard plastic and feel very sturdy. The front of each speaker has an exposed tweeter and woofer. </p><p>At the back of each speaker, there’s a thread for mounting it to the <a href="https://kantoaudio.com/speaker-accessories/su2/" target="_blank" rel="nofollow">Kanto SU2 speaker stands, which are available for $29</a>. This addition will lift the speakers off the desk, giving you more space and reducing surface reflections, enhancing the quality of your playback. Adding these stands made the listening experience ten times better, and I recommend this addition to anyone who buys these speakers. </p><h3 id="controls">Controls</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kaKAQEx5iBXcnZ2JvSVfHC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/kaKAQEx5iBXcnZ2JvSVfHC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The controls on the speakers are very minimal and simplistic, but that makes the experience straightforward. They remind me of the ones I had with my family PC in the 2000s. There's one knob on the front of the main speaker that you press to switch connectivity options and turn to adjust volume. </p><p>At the back of the main speaker, you’ll find the Bluetooth button and a small left/right switch. By toggling this switch, you can switch between the left and right output speakers, meaning you can arrange them however you like on your desk. This made it easy for me to arrange my setup according to where the power outlets in the room are. </p><h3 id="connectivity">Connectivity</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="obHgAU7Q2Y8nDJRttHugqC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/obHgAU7Q2Y8nDJRttHugqC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>At the back of the main speaker, you’ll see all of the ports for the different connectivity options. You can listen to music using Bluetooth 5.3, USB-C or RCA inputs, giving you versatility to use them with computers, phone streaming, or hooked up to a record player. There is also a 3.5mm headphone output port at the front of the speaker. </p><p>The speakers need to be connected to power when in use. There’s a six-foot four-pin speaker connector included in the box to connect the left and right speakers. </p><h2 class="article-body__section" id="section-kanto-uki-review-sound-quality"><span>Kanto UKI review: Sound quality</span></h2><p>I listened using the Kanto UKI speakers connected to my computer via USB-C. I’ve enjoyed listening to my music with the bookshelf speakers every day. Overall, the sound from these speakers is very warm, and the reproduction feels very natural. On their own, they’re perfect for casual listening but lack a wide soundstage; however, when I added the Kanto SU2 Desktop Stands to the setup, the sound was transformed. </p><p>When speakers are placed directly onto a surface like your desk, the sound can be subject to reflection. This means that they begin to sound muffled. But with the addition of speaker stands that can direct the sound upwards, this muffled sound is reduced, and sound reflection diminishes. These stands are a saving grace for these speakers. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CbGGTy7EtwGSTFkSBEu2gC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/CbGGTy7EtwGSTFkSBEu2gC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the bass performance, I listened to <em>Nightmare Tripping </em>by Don Broco. This song relies heavily on the deep bassline, but that wasn’t replicated very well here. Some of the depth was lost, with more focus being put on the vocals. But this is to be expected with these speakers as they bottom out at 80Hz and are primarily designed for detailed listening, prioritizing the mids performance. But a deep bass performance is still achievable if that’s what you’re looking for, as you can pair the speakers with a small sub offered by <a href="https://www.amazon.com/Kanto-Powered-Subwoofer-Accurate-Ultra-Compact/dp/B0FGJTV73D" target="_blank" rel="nofollow">Kanto, such as the Sub6 for $299</a>.</p><p>The mid-performance was impressive. I listened to <em>Wheels</em> by the Foo Fighters. This song is country-esque (for the Foos) and features powerful drums and a bright electric guitar. The drums were well defined throughout the song, and the electric guitar also sounded very clean.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="F6sbJftqYFz7T64gec6rAD" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/F6sbJftqYFz7T64gec6rAD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Finally, I listened to <em>Next To Me</em> by Kingfishr to test the treble and vocal performance. This song is soft and acoustic, lending itself to showcasing the more delicate performance of the speakers. </p><p>The vocals and harmonies throughout the song were clear with good separation. The guitar and harmonica also showed off the bright sound of the speakers and the warmth they can replicate.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9nCAtLKCBqRE62urcLUj6C" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9nCAtLKCBqRE62urcLUj6C.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I also used the speakers while watching Invincible, an action TV show, and was impressed by the level of detail and how the sound effects moved between the speakers. </p><h2 class="article-body__section" id="section-kanto-uki-review-verdict"><span>Kanto UKI review: Verdict</span></h2><p>The Kanto UKI bookshelf speakers have been a great addition to my desk setup. I can listen to them all day and know I’ll get an enjoyable, consistent sound. The simple controls make it easy to make quick volume adjustments or switch over connectivity. </p><p>The diverse selection of connectivity options is ideal if you want to use the speakers connected to a PC, wirelessly to your phone, or with a more bespoke setup with a turntable. The small size means they’re easy to store, and the cute design and color options are the cherry on top. </p><p>Unfortunately, these speakers struggle with deep bass tracks, but you could spend a little extra on the Kanto Ora instead if that bothers you. The narrow soundstage also won’t be ideal for anyone looking for desktop speakers to support music production or mixing. With that said, these speakers are ideal for casual, everyday listening while working. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/speakers/kanto-uki-review</link>
                                                                            <description>
                            <![CDATA[ The Kanto UKI bookshelf speakers are now a staple on my desk for their great sound and cute looks, but additional purchases make them shine. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">MSFNBe5xW9BjxifcEbW5CR</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/9fC6MnCfrez6EubdcKbjDD-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 04 Sep 2026 14:04:25 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ ashley.thieme@futurenet.com (Ashley Thieme) ]]></author>                    <dc:creator><![CDATA[ Ashley Thieme ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/3AWovHjApwuNrSGRS6WBcL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Ashley Thieme is a staff writer on the Reviews team at Tom’s Guide where she tests out the latest tech so you can know what’s going to be worth your time and money. She has a master’s degree in Magazine Journalism and a bachelor’s degree in Journalism, Media and Sociology from Cardiff University. She has bylines in titles including Women’s Health UK, sharing the latest health and wellness news stories, and Virgin Radio UK, providing the latest entertainment news and working on celebrity interviews. She has experience reporting on a variety of topics including music, literature, motorsport, entertainment and health. In previously published work, she has reviewed live music events, books, and wellness products. She values the importance of tech enhancing your life rather than taking over, and as a music fanatic, she is always looking for the best way to listen to new music releases. Discovering the top audio equipment that enhances sound quality and provides optimum comfort is what Ashley does best. In her spare time, Ashley enjoys hitting her reading goals on Goodreads by getting into the latest novels with a cup of tea as well as getting out in the Welsh mountains for a good hike on the weekend.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/9fC6MnCfrez6EubdcKbjDD-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Kanto UKI bookshelf speakers in sage photographed in front of a blue background]]></media:description>                                                            <media:text><![CDATA[Kanto UKI bookshelf speakers in sage photographed in front of a blue background]]></media:text>
                                <media:title type="plain"><![CDATA[Kanto UKI bookshelf speakers in sage photographed in front of a blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/9fC6MnCfrez6EubdcKbjDD-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>If you’re looking for some of the <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html">best computer speakers</a> to upgrade your desk setup, then look no further than the Kanto UKI bookshelf speakers. Though compact they may be, quiet they are not, with a room-filling sound that excellently replicates mids and treble. </p><p>The controls are simple to get to grips with, and the diverse range of connectivity options means you can use them for everything. From wirelessly streaming from your phone to connecting to a turntable, these speakers are incredibly versatile. </p><p>On their own, the speakers struggle to replicate bass and have a narrow soundstage, but these issues can be fixed with additional purchases of a sub and speaker stands. To find out more, you can keep reading this full Kanto UKI review. </p><h2 class="article-body__section" id="section-kanto-uki-review-specs"><span>Kanto UKI review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSTD2S4M" target="_blank" rel="nofollow">$269</a> / <a href="https://www.amazon.co.uk/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSVVT6GL" target="_blank" rel="nofollow">£262</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, USB-C, RCA, 3.5mm out</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>80 Hz - 22kHz</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (per speaker)</strong></p></td><td  ><p>6.5 x 4.4 x 4.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.2 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Onyx, Chalk, Cornflower, Sage, Pumice</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-kanto-uki-review-price-availability"><span>Kanto UKI review: Price & availability</span></h2><p>The Kanto UKI bookshelf speakers are available for <a href="https://www.amazon.com/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSTD2S4M" target="_blank" rel="nofollow">$269 from Amazon U.S</a>. and <a href="https://www.amazon.co.uk/Kanto-Bluetooth-Intelligent-Integration-Headphone/dp/B0FSVVT6GL" target="_blank" rel="nofollow">£262 from Amazon U.K.</a> The speakers come in five different colors, including Onyx, Chalk, Cornflower, Pumice and Sage, which I tested. </p><p>Kanto also offers the <a href="https://www.tomsguide.com/audio/speakers/kanto-ora">Kanto Ora</a> bookshelf speakers for the higher price of $349. These speakers only come in black, but they offer a wider soundstage and studio sound for music production or mixing. There are also the <a href="https://www.tomsguide.com/audio/kanto-ora4-review">Kanto Ora4</a> for $399, which are the same as the Ora but powered. </p><p>A cheaper way to upgrade your desk setup is the <a href="https://www.tomsguide.com/audio/speakers/jbl-104-bt">JBL 104-BT</a> speakers for $199. They have a wide sound but struggle in the low end. I personally don’t think they look as good as the Kanto speakers, but they do the job just fine. </p><p>If you’re short on space and need to save some space (and money), the <a href="https://www.tomsguide.com/audio/speakers/creative-pebble-pro-review">Creative Pebble Pro </a>speakers cost just $69. The speakers have powerful bass, but I prefer the soundstage of the Kanto UKI speakers. </p><h2 class="article-body__section" id="section-kanto-uki-review-design"><span>Kanto UKI review: Design </span></h2><p>The Kanto UKI speakers are an undeniably stylish duo that has brightened up my desk. I tested the Sage colorway of the speakers, and since my room is decorated with a lot of green furniture, they fit right in. </p><p>Each of the speakers measures 6.5 x 4.4 x 4.3 inches, so they’re very compact. They’re smaller than the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review">Edifier R1280T</a> speakers ($149), which measure 9.2 x 7.7 x 5.7 inches. If you’re short on space, the Kanto speakers are a no-brainer. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CXXy9yLH4MsajcHDK7zLUC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/CXXy9yLH4MsajcHDK7zLUC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>They are made from partially recycled hard plastic and feel very sturdy. The front of each speaker has an exposed tweeter and woofer. </p><p>At the back of each speaker, there’s a thread for mounting it to the <a href="https://kantoaudio.com/speaker-accessories/su2/" target="_blank" rel="nofollow">Kanto SU2 speaker stands, which are available for $29</a>. This addition will lift the speakers off the desk, giving you more space and reducing surface reflections, enhancing the quality of your playback. Adding these stands made the listening experience ten times better, and I recommend this addition to anyone who buys these speakers. </p><h3 id="controls">Controls</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kaKAQEx5iBXcnZ2JvSVfHC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/kaKAQEx5iBXcnZ2JvSVfHC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The controls on the speakers are very minimal and simplistic, but that makes the experience straightforward. They remind me of the ones I had with my family PC in the 2000s. There's one knob on the front of the main speaker that you press to switch connectivity options and turn to adjust volume. </p><p>At the back of the main speaker, you’ll find the Bluetooth button and a small left/right switch. By toggling this switch, you can switch between the left and right output speakers, meaning you can arrange them however you like on your desk. This made it easy for me to arrange my setup according to where the power outlets in the room are. </p><h3 id="connectivity">Connectivity</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="obHgAU7Q2Y8nDJRttHugqC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/obHgAU7Q2Y8nDJRttHugqC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>At the back of the main speaker, you’ll see all of the ports for the different connectivity options. You can listen to music using Bluetooth 5.3, USB-C or RCA inputs, giving you versatility to use them with computers, phone streaming, or hooked up to a record player. There is also a 3.5mm headphone output port at the front of the speaker. </p><p>The speakers need to be connected to power when in use. There’s a six-foot four-pin speaker connector included in the box to connect the left and right speakers. </p><h2 class="article-body__section" id="section-kanto-uki-review-sound-quality"><span>Kanto UKI review: Sound quality</span></h2><p>I listened using the Kanto UKI speakers connected to my computer via USB-C. I’ve enjoyed listening to my music with the bookshelf speakers every day. Overall, the sound from these speakers is very warm, and the reproduction feels very natural. On their own, they’re perfect for casual listening but lack a wide soundstage; however, when I added the Kanto SU2 Desktop Stands to the setup, the sound was transformed. </p><p>When speakers are placed directly onto a surface like your desk, the sound can be subject to reflection. This means that they begin to sound muffled. But with the addition of speaker stands that can direct the sound upwards, this muffled sound is reduced, and sound reflection diminishes. These stands are a saving grace for these speakers. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CbGGTy7EtwGSTFkSBEu2gC" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/CbGGTy7EtwGSTFkSBEu2gC.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the bass performance, I listened to <em>Nightmare Tripping </em>by Don Broco. This song relies heavily on the deep bassline, but that wasn’t replicated very well here. Some of the depth was lost, with more focus being put on the vocals. But this is to be expected with these speakers as they bottom out at 80Hz and are primarily designed for detailed listening, prioritizing the mids performance. But a deep bass performance is still achievable if that’s what you’re looking for, as you can pair the speakers with a small sub offered by <a href="https://www.amazon.com/Kanto-Powered-Subwoofer-Accurate-Ultra-Compact/dp/B0FGJTV73D" target="_blank" rel="nofollow">Kanto, such as the Sub6 for $299</a>.</p><p>The mid-performance was impressive. I listened to <em>Wheels</em> by the Foo Fighters. This song is country-esque (for the Foos) and features powerful drums and a bright electric guitar. The drums were well defined throughout the song, and the electric guitar also sounded very clean.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="F6sbJftqYFz7T64gec6rAD" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/F6sbJftqYFz7T64gec6rAD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Finally, I listened to <em>Next To Me</em> by Kingfishr to test the treble and vocal performance. This song is soft and acoustic, lending itself to showcasing the more delicate performance of the speakers. </p><p>The vocals and harmonies throughout the song were clear with good separation. The guitar and harmonica also showed off the bright sound of the speakers and the warmth they can replicate.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9nCAtLKCBqRE62urcLUj6C" name="Kanto UKI" alt="Kanto UKI bookshelf speakers in sage photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9nCAtLKCBqRE62urcLUj6C.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I also used the speakers while watching Invincible, an action TV show, and was impressed by the level of detail and how the sound effects moved between the speakers. </p><h2 class="article-body__section" id="section-kanto-uki-review-verdict"><span>Kanto UKI review: Verdict</span></h2><p>The Kanto UKI bookshelf speakers have been a great addition to my desk setup. I can listen to them all day and know I’ll get an enjoyable, consistent sound. The simple controls make it easy to make quick volume adjustments or switch over connectivity. </p><p>The diverse selection of connectivity options is ideal if you want to use the speakers connected to a PC, wirelessly to your phone, or with a more bespoke setup with a turntable. The small size means they’re easy to store, and the cute design and color options are the cherry on top. </p><p>Unfortunately, these speakers struggle with deep bass tracks, but you could spend a little extra on the Kanto Ora instead if that bothers you. The narrow soundstage also won’t be ideal for anyone looking for desktop speakers to support music production or mixing. With that said, these speakers are ideal for casual, everyday listening while working. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I listened to 10 of Sony’s new ULT party speakers together — and they offer something for everyone ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Today, on 1 September 2026, Sony introduced three new party speakers: Sony ULT Tower 5, Sony ULT Tower 7, and Sony ULT Tower Max. They build on the success of the previous generation of ULT Tower speakers, like the Tower 10, while introducing new, fun features that bring parties, DJ sets, and karaoke sessions to life.</p><p>The second generation features a new design philosophy and more sophisticated reactive lighting, a new ULT button on the front of each speaker (for changing EQ presets), 360° sound, and better, more powerful drivers. Battery life has been improved across all three speakers, too, as has durability and portability.</p><p>I love testing party speakers because I love headbanging to my favorite tunes. Could the ULT Towers make me love them more than I adore the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Marshall Bromley 750</a> and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Marshall Bromley 450</a>? While I haven't had time to do full reviews of the new speakers, I've spent just under an hour listening to them at a pre-launch event. In fact, I listened to <em>10 </em>of them together. My first impressions? Quite positive, so allow me to share with you what I think of the Tower 5, Tower 7 and Tower Max. </p><h2 id="meet-the-ult-tower-max-tower-7-and-tower-5">Meet the ULT Tower Max, Tower 7 and Tower 5</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3000px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="ng8LAPWC4c8Sn4sniJbkAC" name="Sony_ULT_speakers-5" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/ng8LAPWC4c8Sn4sniJbkAC.jpg" mos="" align="middle" fullscreen="" width="3000" height="1687" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>The Sony ULT Tower Max, Tower 7 and Tower 5 are bass-first party speakers that, according to Sony, deliver the most powerful bass yet in the brand's ULT Power Sound series. All three speakers are designed for music lovers, and they combine deep bass, high sound pressure, and party features for an unforgettable experience.</p><p>Regardless of the speaker you buy, you'll be getting XLR, RCA, USB-C and USB-A connectivity for hooking up mics for karaoke, DJ mixers, as well as instruments — making the ULT Towers perfect for nearly every occasion. Each speaker is also fitted with four handles and wheels for easy transport... though you may still need another pair of hands to help with the gargantuan Max.</p><p>The Tower Max, Tower 7 and Tower 5 are IPX4-rated so they're splash-proof. I'm surprised that they aren't more water-resistant, like the IP54-rated Marshall Bromley 750. That will limit the speakers' use outdoors — but really, you shouldn't be using electronic devices in the pouring rain anyway.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2566px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="gQ6MfgWD9B6XkqPcmXvquC" name="Sony_ULT_speakers-2" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/v2/t:0,l:0,cw:2566,ch:1443,q:80/gQ6MfgWD9B6XkqPcmXvquC.png" mos="" align="middle" fullscreen="" width="2566" height="1446" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>Sony has worked with four acclaimed (and Grammy-nominated) hip-hop sound creators — Dot Da Genius, Daru Jones, DJ Dready, and Dennis "ROC.am" Jones — to fine-tune the ULT Towers' driver setup and sound profile. As a result, the speakers deliver distortion-free sound across the volume scale.</p><p>The Tower Max packs three 280mm woofers, two mid-range drivers, and four tweeters, so you're getting a lot of power. The Tower 7 is fitted with a single 280mm woofer, two mid-range drivers, and four tweeters, too. Lastly, the Tower 5 features a 230mm woofer, two mid-range drivers, and two tweeters. That's <em>a lot </em>for the lineup's smallest speaker!</p><p>You also get a customizable 10-band equalizer with all three speakers, and the new ULT button on the front grille now lets you swap between EQ presets. These include ULT1 (deep bass, low frequency sound), ULT2 (rumbling bass), Flat (true-to-source), and Live (spacious).</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7070px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vfgsx9yAq73j7SRCCi4VBD" name="Sony_ULT_speakers-3" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/vfgsx9yAq73j7SRCCi4VBD.jpg" mos="" align="middle" fullscreen="" width="7070" height="3977" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>There's really no dearth of features, which is excellent. And the ULT Towers sound like great value for money, too, as they aren't that expensive. I was personally quite surprised when the Sony representatives shared the retail prices with me. The Tower Max is the most expensive of the lot, at <a href="https://www.amazon.com/dp/B0HF5Z52X6" target="_blank" rel="nofollow">$1,499</a> / ~ £1,300; the Tower 7 is the mid-range option, at <a href="https://www.amazon.com/dp/B0HF6FGCZ3" target="_blank" rel="nofollow">$649</a> / ~ £600; and the Tower 5 is the cheapest option, at <a href="https://www.amazon.com/dp/B0HF67ZGC2" target="_blank" rel="nofollow">$449</a> / ~ £400.</p><h2 id="so-what-do-they-sound-like">So, what do they sound like?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1996px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="SrCNanAfRwUm7NzVb2UonB" name="Sony_ULT_speakers-6" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/SrCNanAfRwUm7NzVb2UonB.jpg" mos="" align="middle" fullscreen="" width="1996" height="1123" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>Driver sizes and specifications are all for naught if the speakers don't sound great — and in my short time with the Sony ULT Towers, I thought they sounded fantastic. At the pre-launch event, the Sony representatives played songs from different genres to show what the Tower Max, Tower 7 and Tower 5 were capable of.</p><p>Regardless of the song they played, I found myself grinning from ear to ear. All three speakers sounded beautiful. The bass sounded rich and meaty with plenty of low-end impact, and I thoroughly enjoyed the ULT2 EQ preset which made me feel the bass in my chest, temples and deep within my soul.</p><p>The Sony representatives played a song by Dua Lipa, too, and though I'm personally not a huge fan of hers, I couldn't help but tap my feet and bob my head along to the tune. I believe that's a true sign of a competent party speaker. If it can make you dance to artists you don't care for, it's done its job.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3000px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="qJxYLxmSggyBmSkTfBKafC" name="Sony_ULT_speakers-4" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/qJxYLxmSggyBmSkTfBKafC.jpg" mos="" align="middle" fullscreen="" width="3000" height="1687" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>I also listened to two Tower 7s together in stereo mode with left and right channels. When 'Money' by Pink Floyd started playing and the sound of cash registers and coins traveled between the speakers, I was left extremely impressed. The high-pitched coins didn't sound like they were piercing through the soundstage. The treble sounded shiny and vibrant but not overzealous or fatiguing.</p><p>Finally, I listened to an electronic track on 10 ULT Towers all at once. It's not everyday you get to do something like that — how many of us have such extravagant setups in our basements, eh? The wall of speakers sounded phenomenal. The room-shaking bass made me feel like I was at a gig or a music festival.</p><h2 id="looking-ahead">Looking ahead...</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7070px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vfgsx9yAq73j7SRCCi4VBD" name="Sony_ULT_speakers-3" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/vfgsx9yAq73j7SRCCi4VBD.jpg" mos="" align="middle" fullscreen="" width="7070" height="3977" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>So, Sony's latest generation of party speakers sounds excellent — on paper and in real-world use. The Tower Max, Tower 7 and Tower 5 offer something for everyone, and given how affordable they are (especially the Tower 5), they offer bang for your buck. That's based on my first impressions, of course, and it's subject to change when I do my in-depth testing.</p><p>Speaking of, I'm really looking forward to putting the Tower Max, Tower 7 and Tower 5 through their paces. Do these party speakers sound like they'd be up your street? Let me know in the comments below!</p><p><em>The Sony ULT Tower Max, Tower 7 and Tower 5 will be available to buy from Amazon U.S. and U.K., Best Buy, or </em><a href="http://sony.com/" target="_blank" rel="nofollow"><em>directly from Sony</em></a><em>.</em></p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-versus-marshall-bromley-750-face-off">I tested the Marshall Bromley 450 and the Marshall Bromley 750 party speakers — and the winner surprised me</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/forget-the-bose-s1-pro-the-brand-new-marshall-bromley-450-just-redefined-party-speakers">There's no better party speaker on the planet than the brand-new Marshall Bromley 450</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/as-a-guitarist-and-an-audiophile-this-is-the-all-in-one-speaker-ive-been-waiting-for">As a guitarist and an audiophile, this is the all-in-one speaker I’ve been waiting for</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/sony-ult-tower-party-speakers-hands-on</link>
                                                                            <description>
                            <![CDATA[ Sony has unveiled its next generation of party speakers: the ULT Tower Max, ULT Tower 7 and ULT Tower 5 are here, and they boast some impressive features. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">HkohLSDZE7NEPsGe8jpWvK</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/QnKknrtrJM5M99V3wB73w8-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 01 Sep 2026 16:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/QnKknrtrJM5M99V3wB73w8-1280-80.jpg">
                                                            <media:credit><![CDATA[Nikita Achanta / Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[An arrangement of Sony ULT Tower 5, ULT Tower 7 and ULT Tower Max]]></media:description>                                                            <media:text><![CDATA[An arrangement of Sony ULT Tower 5, ULT Tower 7 and ULT Tower Max]]></media:text>
                                <media:title type="plain"><![CDATA[An arrangement of Sony ULT Tower 5, ULT Tower 7 and ULT Tower Max]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/QnKknrtrJM5M99V3wB73w8-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Today, on 1 September 2026, Sony introduced three new party speakers: Sony ULT Tower 5, Sony ULT Tower 7, and Sony ULT Tower Max. They build on the success of the previous generation of ULT Tower speakers, like the Tower 10, while introducing new, fun features that bring parties, DJ sets, and karaoke sessions to life.</p><p>The second generation features a new design philosophy and more sophisticated reactive lighting, a new ULT button on the front of each speaker (for changing EQ presets), 360° sound, and better, more powerful drivers. Battery life has been improved across all three speakers, too, as has durability and portability.</p><p>I love testing party speakers because I love headbanging to my favorite tunes. Could the ULT Towers make me love them more than I adore the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Marshall Bromley 750</a> and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Marshall Bromley 450</a>? While I haven't had time to do full reviews of the new speakers, I've spent just under an hour listening to them at a pre-launch event. In fact, I listened to <em>10 </em>of them together. My first impressions? Quite positive, so allow me to share with you what I think of the Tower 5, Tower 7 and Tower Max. </p><h2 id="meet-the-ult-tower-max-tower-7-and-tower-5">Meet the ULT Tower Max, Tower 7 and Tower 5</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3000px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="ng8LAPWC4c8Sn4sniJbkAC" name="Sony_ULT_speakers-5" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/ng8LAPWC4c8Sn4sniJbkAC.jpg" mos="" align="middle" fullscreen="" width="3000" height="1687" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>The Sony ULT Tower Max, Tower 7 and Tower 5 are bass-first party speakers that, according to Sony, deliver the most powerful bass yet in the brand's ULT Power Sound series. All three speakers are designed for music lovers, and they combine deep bass, high sound pressure, and party features for an unforgettable experience.</p><p>Regardless of the speaker you buy, you'll be getting XLR, RCA, USB-C and USB-A connectivity for hooking up mics for karaoke, DJ mixers, as well as instruments — making the ULT Towers perfect for nearly every occasion. Each speaker is also fitted with four handles and wheels for easy transport... though you may still need another pair of hands to help with the gargantuan Max.</p><p>The Tower Max, Tower 7 and Tower 5 are IPX4-rated so they're splash-proof. I'm surprised that they aren't more water-resistant, like the IP54-rated Marshall Bromley 750. That will limit the speakers' use outdoors — but really, you shouldn't be using electronic devices in the pouring rain anyway.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2566px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="gQ6MfgWD9B6XkqPcmXvquC" name="Sony_ULT_speakers-2" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/v2/t:0,l:0,cw:2566,ch:1443,q:80/gQ6MfgWD9B6XkqPcmXvquC.png" mos="" align="middle" fullscreen="" width="2566" height="1446" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>Sony has worked with four acclaimed (and Grammy-nominated) hip-hop sound creators — Dot Da Genius, Daru Jones, DJ Dready, and Dennis "ROC.am" Jones — to fine-tune the ULT Towers' driver setup and sound profile. As a result, the speakers deliver distortion-free sound across the volume scale.</p><p>The Tower Max packs three 280mm woofers, two mid-range drivers, and four tweeters, so you're getting a lot of power. The Tower 7 is fitted with a single 280mm woofer, two mid-range drivers, and four tweeters, too. Lastly, the Tower 5 features a 230mm woofer, two mid-range drivers, and two tweeters. That's <em>a lot </em>for the lineup's smallest speaker!</p><p>You also get a customizable 10-band equalizer with all three speakers, and the new ULT button on the front grille now lets you swap between EQ presets. These include ULT1 (deep bass, low frequency sound), ULT2 (rumbling bass), Flat (true-to-source), and Live (spacious).</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7070px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vfgsx9yAq73j7SRCCi4VBD" name="Sony_ULT_speakers-3" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/vfgsx9yAq73j7SRCCi4VBD.jpg" mos="" align="middle" fullscreen="" width="7070" height="3977" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>There's really no dearth of features, which is excellent. And the ULT Towers sound like great value for money, too, as they aren't that expensive. I was personally quite surprised when the Sony representatives shared the retail prices with me. The Tower Max is the most expensive of the lot, at <a href="https://www.amazon.com/dp/B0HF5Z52X6" target="_blank" rel="nofollow">$1,499</a> / ~ £1,300; the Tower 7 is the mid-range option, at <a href="https://www.amazon.com/dp/B0HF6FGCZ3" target="_blank" rel="nofollow">$649</a> / ~ £600; and the Tower 5 is the cheapest option, at <a href="https://www.amazon.com/dp/B0HF67ZGC2" target="_blank" rel="nofollow">$449</a> / ~ £400.</p><h2 id="so-what-do-they-sound-like">So, what do they sound like?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1996px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="SrCNanAfRwUm7NzVb2UonB" name="Sony_ULT_speakers-6" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/SrCNanAfRwUm7NzVb2UonB.jpg" mos="" align="middle" fullscreen="" width="1996" height="1123" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>Driver sizes and specifications are all for naught if the speakers don't sound great — and in my short time with the Sony ULT Towers, I thought they sounded fantastic. At the pre-launch event, the Sony representatives played songs from different genres to show what the Tower Max, Tower 7 and Tower 5 were capable of.</p><p>Regardless of the song they played, I found myself grinning from ear to ear. All three speakers sounded beautiful. The bass sounded rich and meaty with plenty of low-end impact, and I thoroughly enjoyed the ULT2 EQ preset which made me feel the bass in my chest, temples and deep within my soul.</p><p>The Sony representatives played a song by Dua Lipa, too, and though I'm personally not a huge fan of hers, I couldn't help but tap my feet and bob my head along to the tune. I believe that's a true sign of a competent party speaker. If it can make you dance to artists you don't care for, it's done its job.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3000px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="qJxYLxmSggyBmSkTfBKafC" name="Sony_ULT_speakers-4" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/qJxYLxmSggyBmSkTfBKafC.jpg" mos="" align="middle" fullscreen="" width="3000" height="1687" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>I also listened to two Tower 7s together in stereo mode with left and right channels. When 'Money' by Pink Floyd started playing and the sound of cash registers and coins traveled between the speakers, I was left extremely impressed. The high-pitched coins didn't sound like they were piercing through the soundstage. The treble sounded shiny and vibrant but not overzealous or fatiguing.</p><p>Finally, I listened to an electronic track on 10 ULT Towers all at once. It's not everyday you get to do something like that — how many of us have such extravagant setups in our basements, eh? The wall of speakers sounded phenomenal. The room-shaking bass made me feel like I was at a gig or a music festival.</p><h2 id="looking-ahead">Looking ahead...</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7070px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vfgsx9yAq73j7SRCCi4VBD" name="Sony_ULT_speakers-3" alt="Sony ULT Tower party speakers" src="https://cdn.mos.cms.futurecdn.net/vfgsx9yAq73j7SRCCi4VBD.jpg" mos="" align="middle" fullscreen="" width="7070" height="3977" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sony)</span></figcaption></figure><p>So, Sony's latest generation of party speakers sounds excellent — on paper and in real-world use. The Tower Max, Tower 7 and Tower 5 offer something for everyone, and given how affordable they are (especially the Tower 5), they offer bang for your buck. That's based on my first impressions, of course, and it's subject to change when I do my in-depth testing.</p><p>Speaking of, I'm really looking forward to putting the Tower Max, Tower 7 and Tower 5 through their paces. Do these party speakers sound like they'd be up your street? Let me know in the comments below!</p><p><em>The Sony ULT Tower Max, Tower 7 and Tower 5 will be available to buy from Amazon U.S. and U.K., Best Buy, or </em><a href="http://sony.com/" target="_blank" rel="nofollow"><em>directly from Sony</em></a><em>.</em></p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-versus-marshall-bromley-750-face-off">I tested the Marshall Bromley 450 and the Marshall Bromley 750 party speakers — and the winner surprised me</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/forget-the-bose-s1-pro-the-brand-new-marshall-bromley-450-just-redefined-party-speakers">There's no better party speaker on the planet than the brand-new Marshall Bromley 450</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/as-a-guitarist-and-an-audiophile-this-is-the-all-in-one-speaker-ive-been-waiting-for">As a guitarist and an audiophile, this is the all-in-one speaker I’ve been waiting for</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Sonos is ready to steal back the soundbar crown with the new Beam Ultra ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Sonos is no stranger to the world of soundbars. The <a href="https://www.tomsguide.com/audio/soundbars/sonos-arc-ultra-review">Sonos Arc Ultra</a> is widely regarded as one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a> on the market — but its $1,100 sticker price makes it a tough sell when wallets are stretched thin. </p><p>That’s where the new Sonos Beam Ultra coms in. Announced early at an event in New York City on September 1, the new mid-range soundbar sports two dedicated up-firing drivers and a redesigned center channel that deliver clearer dialogue.</p><p>From everything I've seen on it, the Beam Ultra is a compact 7.1.2 Dolby Atmos soundbar that fits everything I love about its more-expensive sibling into a $700 price point. Here’s what makes it the soundbar to watch when it becomes available on September 29.</p><h2 id="big-sound-small-package">Big sound, small package</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1490px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="oRmpa3Rxs8pF6uqDRfdU7X" name="Beam Ultra 8.JPG" alt="A black Sonos Beam Ultra on an entertainment stand." src="https://cdn.mos.cms.futurecdn.net/oRmpa3Rxs8pF6uqDRfdU7X.jpg" mos="" align="middle" fullscreen="" width="1490" height="838" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sonos)</span></figcaption></figure><p>The current-generation <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2">Sonos Beam (2nd Gen)</a> does a great job with its 5.0-channel configuration — but the Beam Ultra uses nine speakers, including the two additional up-firing drivers, that are going to make a world of difference for surround sound movie soundtracks. </p><p>While the new Beam Ultra won’t ship with any satellite speakers or subwoofers, the Beam Ultra is compatible with the whole Sonos ecosystem including, of course, the Sonos Sub and <a href="https://www.tomsguide.com/reviews/sonos-sub-mini">Sonos Sub Mini</a> that can be used for a meatier low-end. </p><p>Sonos says the newly designed center channel driver enables clearer dialogue and can tap into four levels of AI Speech Enhancement for even more pronounced mids, as well as a Night Sound mode when you want to cut back on any wall-rattling frequencies.</p><p>Last but not least, Sonos says the new Beam Ultra will support the brand’s Trueplay technology that takes an aural image of the room and adjusts the output of the sound accordingly. It’s a handy feature, though one that never quite matches the effects of proper sound dampening in my experience.</p><h2 id="the-beam-ultra-offers-both-brains-and-brawn">The Beam Ultra offers both brains and brawn</h2><p>In addition to its revamped internals, the Sonos Beam Ultra will leverage the company’s new Sonos 27 smart platform. Those words might scare you considering that the last time the company launched a new smart platform, it went so poorly that it <a href="https://www.tomsguide.com/audio/speakers/sonos-ceo-exits-following-major-app-fail-but-theres-good-news">cost the CEO his job</a> — but Sonos 27 feels like it could actually make the platform better overall. </p><p>Key features in the latest platform are Sonos 27voice and Sonos 27mcp. The former is Sonos’ new voice assistant that will live (I assume) alongside Alexa, while the latter allows you to connect your Sonos ecosystem to any outside AI assistant or agent.</p><p>Eventually, Sonos will allow users to design their own custom agents — with a matching persona and voice — but details on that are relatively slim at the moment. </p><div><blockquote><p>Eventually, Sonos will allow users to design their own custom agents — with a matching persona and voice</p></blockquote></div><p>Finally, anyone who uses Sonos portable speakers like the <a href="https://www.tomsguide.com/reviews/sonos-move-2">Move 2</a> or <a href="https://www.tomsguide.com/audio/smart-speakers/sonos-play-review">Sonos Play</a> will be able to use them in a future update as surround sound speakers for the Beam Ultra. This feature, dubbed Sonos Fabric, uses high-frequency sound that enables Sonos products to “sense each other” and “adapt to the moment.” </p><p>We'll certainly test that ourselves when the feature becomes available at some point in the future.</p><h2 id="sony-eat-your-heart-out">Sony, eat your heart out</h2><p>Sonos isn't the only one going after the more modest soundbar market — this is Sony, Samsung and LG's bread and butter. </p><p>Each year, the trio of TV-makers deliver a half dozen of sub-$1,000 soundbars into the market, and every year they get gobbled up during sales events like Black Friday and, to a lesser extent, Super Bowl Sunday. </p><p>On paper, the Sonos Beam Ultra delivers a lot of value — potentially a whole lot more than what the competition offers — but $700 is still a pretty hefty chunk of change. </p><p>Is it worth the scratch? We'll know more after we hear it for ourselves at the New York event and bring it into our own homes for comparison testing sometime around its September 29 launch date.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/earbuds/noble-audio-iris-iems-review">I tested Noble's high-end IEMs and they boast an unbelievably wide soundstage — but are the Iris worth $699?</a></li><li><a href="https://www.tomsguide.com/audio/soundbars/sony-bravia-theater-bar-7-review">I tested the Sony Bravia Theater Bar 7 + subwoofer — and recommending it is a no-brainer</a></li><li><a href="https://www.tomsguide.com/audio/speakers/before-you-buy-your-next-pair-of-speakers-make-sure-you-know-this-one-essential-difference">Before you buy your next pair of speakers, make sure you know this one essential difference</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/sonos-is-ready-to-steal-back-the-soundbar-crown-with-the-new-beam-ultra</link>
                                                                            <description>
                            <![CDATA[ Time to upgrade your soundbar? The all-new Sonos Beam Ultra delivers 7.1.2 Dolby Atmos sound for under $700. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">542a3j8jzGJ78F9xBKfrdQ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/LprZFdxoeDbSM5yL2azuYP-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 01 Sep 2026 14:40:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ Nick.Pino@futurenet.com (Nick Pino) ]]></author>                    <dc:creator><![CDATA[ Nick Pino ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/7xnnEdyK5eEbDVbS5pYB54.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nick Pino heads up the TV and AV verticals at Tom&#039;s Guide and covers everything from OLED TVs to the latest wireless headphones. He&#039;s a Level 1 Certified THX Home Theater Professional, and has attended classes on TV and audio calibration. On Tom&#039;s Guide, he looks after some of the site&#039;s important buying guides like the best TVs, best OLED TVs, best 4K TVs, so most of his day is spent watching and evaluating new screens from LG, Samsung, Sony, Hisense, TCL and Vizio. (And yes, he knows how lucky he is to say that.) He was formerly the Senior Editor, TV and AV at TechRadar (Tom&#039;s Guide&#039;s sister site) and has previously written for GamesRadar, Official Xbox Magazine, PC Gamer and other outlets over the last decade. He got his start on Best Buy&#039;s official video game magazine, @GAMER, for whom he attended his first E3 in 2013. He&#039;s served as a judge for CES&#039; prestigious Innovation Awards and has attended the tech conference in Las Vegas since 2014. He has a computer science degree from the University at Buffalo and still resides in the Buffalo area. (Go Bills!) Not sure which TV you should buy? Drop him an email or tweet him on Twitter and he can help you out.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/LprZFdxoeDbSM5yL2azuYP-1280-80.jpg">
                                                            <media:credit><![CDATA[Sonos]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The new Sonos Beam Ultra on an entertainment center.]]></media:description>                                                            <media:text><![CDATA[The new Sonos Beam Ultra on an entertainment center.]]></media:text>
                                <media:title type="plain"><![CDATA[The new Sonos Beam Ultra on an entertainment center.]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/LprZFdxoeDbSM5yL2azuYP-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Sonos is no stranger to the world of soundbars. The <a href="https://www.tomsguide.com/audio/soundbars/sonos-arc-ultra-review">Sonos Arc Ultra</a> is widely regarded as one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a> on the market — but its $1,100 sticker price makes it a tough sell when wallets are stretched thin. </p><p>That’s where the new Sonos Beam Ultra coms in. Announced early at an event in New York City on September 1, the new mid-range soundbar sports two dedicated up-firing drivers and a redesigned center channel that deliver clearer dialogue.</p><p>From everything I've seen on it, the Beam Ultra is a compact 7.1.2 Dolby Atmos soundbar that fits everything I love about its more-expensive sibling into a $700 price point. Here’s what makes it the soundbar to watch when it becomes available on September 29.</p><h2 id="big-sound-small-package">Big sound, small package</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1490px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="oRmpa3Rxs8pF6uqDRfdU7X" name="Beam Ultra 8.JPG" alt="A black Sonos Beam Ultra on an entertainment stand." src="https://cdn.mos.cms.futurecdn.net/oRmpa3Rxs8pF6uqDRfdU7X.jpg" mos="" align="middle" fullscreen="" width="1490" height="838" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Sonos)</span></figcaption></figure><p>The current-generation <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2">Sonos Beam (2nd Gen)</a> does a great job with its 5.0-channel configuration — but the Beam Ultra uses nine speakers, including the two additional up-firing drivers, that are going to make a world of difference for surround sound movie soundtracks. </p><p>While the new Beam Ultra won’t ship with any satellite speakers or subwoofers, the Beam Ultra is compatible with the whole Sonos ecosystem including, of course, the Sonos Sub and <a href="https://www.tomsguide.com/reviews/sonos-sub-mini">Sonos Sub Mini</a> that can be used for a meatier low-end. </p><p>Sonos says the newly designed center channel driver enables clearer dialogue and can tap into four levels of AI Speech Enhancement for even more pronounced mids, as well as a Night Sound mode when you want to cut back on any wall-rattling frequencies.</p><p>Last but not least, Sonos says the new Beam Ultra will support the brand’s Trueplay technology that takes an aural image of the room and adjusts the output of the sound accordingly. It’s a handy feature, though one that never quite matches the effects of proper sound dampening in my experience.</p><h2 id="the-beam-ultra-offers-both-brains-and-brawn">The Beam Ultra offers both brains and brawn</h2><p>In addition to its revamped internals, the Sonos Beam Ultra will leverage the company’s new Sonos 27 smart platform. Those words might scare you considering that the last time the company launched a new smart platform, it went so poorly that it <a href="https://www.tomsguide.com/audio/speakers/sonos-ceo-exits-following-major-app-fail-but-theres-good-news">cost the CEO his job</a> — but Sonos 27 feels like it could actually make the platform better overall. </p><p>Key features in the latest platform are Sonos 27voice and Sonos 27mcp. The former is Sonos’ new voice assistant that will live (I assume) alongside Alexa, while the latter allows you to connect your Sonos ecosystem to any outside AI assistant or agent.</p><p>Eventually, Sonos will allow users to design their own custom agents — with a matching persona and voice — but details on that are relatively slim at the moment. </p><div><blockquote><p>Eventually, Sonos will allow users to design their own custom agents — with a matching persona and voice</p></blockquote></div><p>Finally, anyone who uses Sonos portable speakers like the <a href="https://www.tomsguide.com/reviews/sonos-move-2">Move 2</a> or <a href="https://www.tomsguide.com/audio/smart-speakers/sonos-play-review">Sonos Play</a> will be able to use them in a future update as surround sound speakers for the Beam Ultra. This feature, dubbed Sonos Fabric, uses high-frequency sound that enables Sonos products to “sense each other” and “adapt to the moment.” </p><p>We'll certainly test that ourselves when the feature becomes available at some point in the future.</p><h2 id="sony-eat-your-heart-out">Sony, eat your heart out</h2><p>Sonos isn't the only one going after the more modest soundbar market — this is Sony, Samsung and LG's bread and butter. </p><p>Each year, the trio of TV-makers deliver a half dozen of sub-$1,000 soundbars into the market, and every year they get gobbled up during sales events like Black Friday and, to a lesser extent, Super Bowl Sunday. </p><p>On paper, the Sonos Beam Ultra delivers a lot of value — potentially a whole lot more than what the competition offers — but $700 is still a pretty hefty chunk of change. </p><p>Is it worth the scratch? We'll know more after we hear it for ourselves at the New York event and bring it into our own homes for comparison testing sometime around its September 29 launch date.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/earbuds/noble-audio-iris-iems-review">I tested Noble's high-end IEMs and they boast an unbelievably wide soundstage — but are the Iris worth $699?</a></li><li><a href="https://www.tomsguide.com/audio/soundbars/sony-bravia-theater-bar-7-review">I tested the Sony Bravia Theater Bar 7 + subwoofer — and recommending it is a no-brainer</a></li><li><a href="https://www.tomsguide.com/audio/speakers/before-you-buy-your-next-pair-of-speakers-make-sure-you-know-this-one-essential-difference">Before you buy your next pair of speakers, make sure you know this one essential difference</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Sonos 27 is here to clean up the app mess (again) — this time with AI ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Sonos is giving its audio operating system a huge update that adds several AI tools and a speaker sensing tool that could change how you use your speakers: Dubbed Sonos 27 and announced at an event in NYC, this is the latest version of the company's control app and promises to be open and adaptable. </p><p>"We built our system around a shared love of music and the belief that nothing sets the mood like great sound," CEO Tom Conrad said in a press release. "Sonos 27 takes that further, bringing acoustic engineering, adaptable software,  intelligence and design together in a way only Sonos can."</p><p>The last time Sonos attempted an overhaul, the controversial redesign <a href="https://www.tomsguide.com/audio/speakers/sonos-ceo-exits-following-major-app-fail-but-theres-good-news">cost CEO Patrick Spence his job</a>. After public outcry, it took nearly a year for <a href="https://www.tomsguide.com/audio/speakers/it-works-new-sonos-update-might-have-just-fixed-this-huge-app-headache">Sonos to bring the app back</a> to an acceptable state. Here's hoping history isn't about to repeat itself with Sonos 27.</p><h2 id="what-39-s-new-with-sonos-27">What's new with Sonos 27? </h2><p>Sonos 27 comes with two AI features, Sonos 27voice and Sonos 27mcp, both of which utilize AI to change how controlling Sonos systems works.</p><p>Voice is a Sonos-built assistant for music and mood curation. The company is also introducing Sonos Custom Agents, which would allow users to create their own AI agents through the system, "each with its own model, persona and voice." Mcp lets you connect an outside AI chatbot to your Sonos system, including OpenAI's ChatGPT. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OqgPZe"></div>                            </div>                            <script src="https://kwizly.com/embed/OqgPZe.js" async></script><div><blockquote><p>Mcp lets you connect an outside AI chatbot to your Sonos system, including OpenAI's ChatGPT. </p></blockquote></div><p>Sonos claims you could ask ChatGPT to play music anywhere in your home without leaving your AI chat. </p><p>Sonos didn't provide many details in its press release on how Custom Agents or AI assistant integration will actually work or how you'll connect them to your system, but these details are likely to be discussed during the event in New York City.</p><h2 id="other-more-interesting-features">Other more interesting features</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4594px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Gingaw7NhxbjL9HBYDunjV" name="IMG_6876.JPG" alt="Sonos Play" src="https://cdn.mos.cms.futurecdn.net/Gingaw7NhxbjL9HBYDunjV.jpg" mos="" align="middle" fullscreen="" width="4594" height="2584" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>One of the more interesting features Sonos announced is called Sonos Fabric, a system capability that is supposed to enable Sonos devices to "sense each other" and then adapt to positioning. </p><p>According to Sonos, the positioning technology uses high-frequency sound to sense a speaker's location and then adapt its role in the system based on that. </p><p>Why is this necessary? It specifically enables portable surround sound features that let the <a href="https://www.tomsguide.com/reviews/sonos-move-2">Move 2</a> or <a href="https://www.tomsguide.com/audio/smart-speakers/sonos-play-review">Sonos Play</a> become rear surrounds when they're moved into position. (It's unclear whether this will be added to non-portable speakers.)</p><p>Another new feature launching in Sonos 27 will allow linking the Sonos Ace Ultra (also unveiled today) directly to your speaker system, letting you push or pull sound to and from speakers and your headphones. This is launching in early access to start, so you'll need to sign up for that.</p><p>Finally, Sonos is adding new features that are supposed to make the app "more intuitive to navigate." Sonos claims these new features were shaped and tested by the Sonos community; hopefully it doesn't break the app like the last update.</p><h2 id="so-when-will-it-be-available">So when will it be available? </h2><p>Sonos 27 will be available for all Sonos S2 products, though some features are only available on certain devices. (More details to come soon.)</p><p>The new app navigation features rolls out on September 8, and Sonos 27mcp launches in early access at the same time. </p><p>This will all roughly align with the launch of two new Sonos products — the Sonos Beam Ultra soundbar and Sonos Ace Ultra headphones — that launch on September 29. The Beam Ultra is an updated version of the <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2">Sonos Beam Gen 2 </a>mid-sized soundbar that will come in at  $699, while the Sonos Ace Ultra headphones are a sequel to the original <a href="https://www.tomsguide.com/audio/over-ear-headphones/sonos-ace">Sonos Ace </a>that will cost $449 at launch. </p><p>Both are available for pre-order now.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/earbuds/noble-audio-iris-iems-review">I tested Noble's high-end IEMs and they boast an unbelievably wide soundstage — but are the Iris worth $699?</a></li><li><a href="https://www.tomsguide.com/audio/soundbars/sony-bravia-theater-bar-7-review">I tested the Sony Bravia Theater Bar 7 + subwoofer — and recommending it is a no-brainer</a></li><li><a href="https://www.tomsguide.com/audio/speakers/before-you-buy-your-next-pair-of-speakers-make-sure-you-know-this-one-essential-difference">Before you buy your next pair of speakers, make sure you know this one essential difference</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/speakers/sonos-27-is-here-to-clean-up-the-app-mess-again-this-time-with-ai</link>
                                                                            <description>
                            <![CDATA[ Sonos has a new version of its audio operating system that integrates AI. Here's how it should work. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">eDXyTteqwQmzJFJ5V26Kz8</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/DSrDWQZRHNMcoHdNE5HMZn-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 01 Sep 2026 14:40:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ scott.younker@futurenet.com (Scott Younker) ]]></author>                    <dc:creator><![CDATA[ Scott Younker ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/RZsUpqcJ6Uj2q83oCUwNhQ.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Scott Younker is the West Coast Reporter at Tom’s Guide. He covers all the latest tech news, including phones, computing and more. He’s been involved in tech since 2011 covering everything from cameras and swimming pool equipment to the latest gaming consoles and smart TVs. He is on a seemingly never ending hunt to build the easiest to use home media system. &lt;/p&gt;&lt;p&gt;Before Tom’s Guide, Scott worked for publications like &lt;em&gt;Digital Trends, Outdoor Photographer, Dead Beats Panel&lt;/em&gt;, and in a brief detour, &lt;em&gt;America’s Funniest Home Videos&lt;/em&gt;. Yes, he has seen more pratfalls, silly pets and ridiculous home movie fails than is reasonably healthy. &lt;/p&gt;&lt;p&gt;When not writing about the latest devices or advances in chipsets, be sure to ask about Scott about disc golf and sustainability, or just if you’re being cheeky, ask about his noodle arm. If you truly want to get nerdy, bring up board games and his ongoing losing streak. &lt;/p&gt;&lt;p&gt;Scott joined Tom&#039;s Guide in 2024 as the West Coast Reporter. He graduated from the School of Journalism at the University of Oregon with a degree in magazine journalism and a minor in communications. While there he blogged or wrote for several magazines including the Fluxx, Ethos and the Oregon Commentator. He briefly wrote and managed a moderately successful blog focused on web comics. &lt;br&gt;Send secure tips on Signal: scyo1.55.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/DSrDWQZRHNMcoHdNE5HMZn-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Sonos Arc Ultra under a TV]]></media:description>                                                            <media:text><![CDATA[Sonos Arc Ultra under a TV]]></media:text>
                                <media:title type="plain"><![CDATA[Sonos Arc Ultra under a TV]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/DSrDWQZRHNMcoHdNE5HMZn-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Sonos is giving its audio operating system a huge update that adds several AI tools and a speaker sensing tool that could change how you use your speakers: Dubbed Sonos 27 and announced at an event in NYC, this is the latest version of the company's control app and promises to be open and adaptable. </p><p>"We built our system around a shared love of music and the belief that nothing sets the mood like great sound," CEO Tom Conrad said in a press release. "Sonos 27 takes that further, bringing acoustic engineering, adaptable software,  intelligence and design together in a way only Sonos can."</p><p>The last time Sonos attempted an overhaul, the controversial redesign <a href="https://www.tomsguide.com/audio/speakers/sonos-ceo-exits-following-major-app-fail-but-theres-good-news">cost CEO Patrick Spence his job</a>. After public outcry, it took nearly a year for <a href="https://www.tomsguide.com/audio/speakers/it-works-new-sonos-update-might-have-just-fixed-this-huge-app-headache">Sonos to bring the app back</a> to an acceptable state. Here's hoping history isn't about to repeat itself with Sonos 27.</p><h2 id="what-39-s-new-with-sonos-27">What's new with Sonos 27? </h2><p>Sonos 27 comes with two AI features, Sonos 27voice and Sonos 27mcp, both of which utilize AI to change how controlling Sonos systems works.</p><p>Voice is a Sonos-built assistant for music and mood curation. The company is also introducing Sonos Custom Agents, which would allow users to create their own AI agents through the system, "each with its own model, persona and voice." Mcp lets you connect an outside AI chatbot to your Sonos system, including OpenAI's ChatGPT. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OqgPZe"></div>                            </div>                            <script src="https://kwizly.com/embed/OqgPZe.js" async></script><div><blockquote><p>Mcp lets you connect an outside AI chatbot to your Sonos system, including OpenAI's ChatGPT. </p></blockquote></div><p>Sonos claims you could ask ChatGPT to play music anywhere in your home without leaving your AI chat. </p><p>Sonos didn't provide many details in its press release on how Custom Agents or AI assistant integration will actually work or how you'll connect them to your system, but these details are likely to be discussed during the event in New York City.</p><h2 id="other-more-interesting-features">Other more interesting features</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4594px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Gingaw7NhxbjL9HBYDunjV" name="IMG_6876.JPG" alt="Sonos Play" src="https://cdn.mos.cms.futurecdn.net/Gingaw7NhxbjL9HBYDunjV.jpg" mos="" align="middle" fullscreen="" width="4594" height="2584" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>One of the more interesting features Sonos announced is called Sonos Fabric, a system capability that is supposed to enable Sonos devices to "sense each other" and then adapt to positioning. </p><p>According to Sonos, the positioning technology uses high-frequency sound to sense a speaker's location and then adapt its role in the system based on that. </p><p>Why is this necessary? It specifically enables portable surround sound features that let the <a href="https://www.tomsguide.com/reviews/sonos-move-2">Move 2</a> or <a href="https://www.tomsguide.com/audio/smart-speakers/sonos-play-review">Sonos Play</a> become rear surrounds when they're moved into position. (It's unclear whether this will be added to non-portable speakers.)</p><p>Another new feature launching in Sonos 27 will allow linking the Sonos Ace Ultra (also unveiled today) directly to your speaker system, letting you push or pull sound to and from speakers and your headphones. This is launching in early access to start, so you'll need to sign up for that.</p><p>Finally, Sonos is adding new features that are supposed to make the app "more intuitive to navigate." Sonos claims these new features were shaped and tested by the Sonos community; hopefully it doesn't break the app like the last update.</p><h2 id="so-when-will-it-be-available">So when will it be available? </h2><p>Sonos 27 will be available for all Sonos S2 products, though some features are only available on certain devices. (More details to come soon.)</p><p>The new app navigation features rolls out on September 8, and Sonos 27mcp launches in early access at the same time. </p><p>This will all roughly align with the launch of two new Sonos products — the Sonos Beam Ultra soundbar and Sonos Ace Ultra headphones — that launch on September 29. The Beam Ultra is an updated version of the <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2">Sonos Beam Gen 2 </a>mid-sized soundbar that will come in at  $699, while the Sonos Ace Ultra headphones are a sequel to the original <a href="https://www.tomsguide.com/audio/over-ear-headphones/sonos-ace">Sonos Ace </a>that will cost $449 at launch. </p><p>Both are available for pre-order now.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/earbuds/noble-audio-iris-iems-review">I tested Noble's high-end IEMs and they boast an unbelievably wide soundstage — but are the Iris worth $699?</a></li><li><a href="https://www.tomsguide.com/audio/soundbars/sony-bravia-theater-bar-7-review">I tested the Sony Bravia Theater Bar 7 + subwoofer — and recommending it is a no-brainer</a></li><li><a href="https://www.tomsguide.com/audio/speakers/before-you-buy-your-next-pair-of-speakers-make-sure-you-know-this-one-essential-difference">Before you buy your next pair of speakers, make sure you know this one essential difference</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I found 9 Sonos deals in Best Buy's Labor Day sale — save up to 21% right now ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Labor Day weekend is almost upon us and the sales have already started. Best Buy is running a wide-range of offers at the moment including one focused on Sonos speakers and soundbars. If you've been planning to upgrade your home entertainment setup, now is a great time to do so.</p><p>Amid the items on offer are <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos Era 300 Smart Speaker</a>, which we rated 4.5 stars out of 5, as well as the <a href="https://www.tomsguide.com/reviews/sonos-sub-mini">Sonos Sub Mini Wireless Subwoofer</a>, which we gave the full 5 stars. These are great devices that will bring high quality audio to your TV and movie-viewing experience, as well as making music really pop. Check out all nine of the available Sonos deals below.</p>        <div class="featured_product_block featured_block_hero" data-id="c0fff3fc-a5f6-11f1-8385-63c96ae4aca8">            <a href="https://www.bestbuy.com/product/sonos-era-300-smart-speaker-with-spatial-audio-each-black/J39H3736FZ/sku/6535189" data-model-name="Era 300 Smart Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/VzpyM36rK6kUAbhXX7PMQ.jpg" alt="Sonos, Sonos - Era 300 Smart Speaker With Spatial Audio (each) - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Era 300 Smart Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff55a-a5f6-11f1-9304-bdbd86e47408">            <a href="https://www.bestbuy.com/product/sonos-era-300-smart-speaker-with-spatial-audio-each-white/J39H3736F6/sku/6535188" data-model-name="Era 300 Smart Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/m4Fq6SgcCkR7A3S89MuRFK.jpg" alt="BRAND, Sonos - Era 300 Smart Speaker with Spatial Audio (Each) - White"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Era 300 Smart Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff730-a5f6-11f1-88a0-ffe4a7085132">            <a href="https://www.bestbuy.com/product/sonos-sub-mini-wireless-subwoofer-white/J39H373SPV/sku/6520133" data-model-name="Sub Mini Wireless Subwoofer" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/Ec4bU9AGPSXVpo2BsK4WBN.jpg" alt="Sonos, Sub Mini Wireless Subwoofer"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Sub Mini Wireless Subwoofer</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff5d2-a5f6-11f1-97bf-83785a68e0b1">            <a href="https://www.bestbuy.com/product/sonos-sub-mini-wireless-subwoofer-black/J39H373SS8/sku/6520132" data-model-name="Sub Mini Wireless Subwoofer" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/4iuCjKhcGC2sEjCvg2mDdP.jpg" alt="Sonos - Sub Mini Wireless Subwoofer - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Sub Mini Wireless Subwoofer</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff6c2-a5f6-11f1-aa6b-7dcc8bf87666">            <a href="https://www.bestbuy.com/product/sonos-beam-gen-2-5-0-channel-smart-soundbar-with-dolby-atmos-white/J39H373LTP/sku/6476040" data-model-name="Beam (gen 2) 5.0 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/XkZPCg9sw8dzAG5orXb8Za.jpg" alt="Sonos, Sonos - Beam (gen 2) 5.0 Channel Smart Soundbar With Dolby Atmos - White"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Beam (gen 2) 5.0 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff474-a5f6-11f1-ab7b-75b6e53fa6b8">            <a href="https://www.bestbuy.com/product/sonos-beam-gen-2-5-0-channel-smart-soundbar-with-dolby-atmos-black/J39H373LWC/sku/6476041" data-model-name="Beam (gen 2) 5.0 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/CUam9LquQFCwn233ofKeu8.jpg" alt="Sonos, Sonos - Beam (gen 2) 5.0 Channel Smart Soundbar With Dolby Atmos - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Beam (gen 2) 5.0 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff4e2-a5f6-11f1-95bf-a18395a8aeb5">            <a href="https://www.bestbuy.com/product/sonos-sub-4-wireless-subwoofer-black/J39H37879G/sku/6597168" data-model-name="Sub 4 Wireless Subwoofer" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/3goSFz7wqBbcLFLC3cjySC.jpg" alt="Sonos, Sonos - Sub 4 Wireless Subwoofer - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Sub 4 Wireless Subwoofer</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff37a-a5f6-11f1-add0-bb8878425934">            <a href="https://www.bestbuy.com/product/sonos-arc-ultra-9-1-4-channel-smart-soundbar-with-dolby-atmos-black/J39H373F2S/sku/6597167" data-model-name="Arc Ultra 9.1.4 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/X6ZBq26yRtkqRM3AaW23vh.jpg" alt="Sonos, Sonos - Arc Ultra 9.1.4 Channel Smart Soundbar With Dolby Atmos - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Arc Ultra 9.1.4 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff64a-a5f6-11f1-9b27-6d4f8bb38486">            <a href="https://www.bestbuy.com/product/sonos-arc-ultra-9-1-4-channel-smart-soundbar-with-dolby-atmos-white/J39H373F2X/sku/6597166" data-model-name="Arc Ultra 9.1.4 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/je9XmwWx7dgUwwiFYXwgoW.jpg" alt="BRAND, Sonos - Arc Ultra 9.1.4 Channel Smart Soundbar with Dolby Atmos - White"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Arc Ultra 9.1.4 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/i-found-9-sonos-deals-in-best-buys-labor-day-sale-save-up-to-21-percent-right-now</link>
                                                                            <description>
                            <![CDATA[ Best Buy's Labor Day sale has slashed prices on a range of Sonos soundbars and speakers. Save up to 21% on these premium devices and build the home theater of your dreams. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">wqSNRDnCYTNPjDJw9qHvVT</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/JV27zGsVxb9E3qxjVLzqvE-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 01 Sep 2026 14:20:37 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                                    <dc:creator><![CDATA[ Will Salmon ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/HpRWP7Mh9ErgRGoTvZBbBJ.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Will Salmon is Tom&#039;s Guide&#039;s Collections Editor, curating advice on the best products to spend your money on. Will has been writing about tech and entertainment for almost 20 years, including stints as GamesRadar+&#039;s Streaming Editor, editing comics website Newsarama, and writing for lots and lots of magazines.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/JV27zGsVxb9E3qxjVLzqvE-1280-80.jpg">
                                                            <media:credit><![CDATA[Sonos / Edited by Gemini]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A selection of Sonos speakers in a room. ]]></media:description>                                                            <media:text><![CDATA[A selection of Sonos speakers in a room. ]]></media:text>
                                <media:title type="plain"><![CDATA[A selection of Sonos speakers in a room. ]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/JV27zGsVxb9E3qxjVLzqvE-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Labor Day weekend is almost upon us and the sales have already started. Best Buy is running a wide-range of offers at the moment including one focused on Sonos speakers and soundbars. If you've been planning to upgrade your home entertainment setup, now is a great time to do so.</p><p>Amid the items on offer are <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos Era 300 Smart Speaker</a>, which we rated 4.5 stars out of 5, as well as the <a href="https://www.tomsguide.com/reviews/sonos-sub-mini">Sonos Sub Mini Wireless Subwoofer</a>, which we gave the full 5 stars. These are great devices that will bring high quality audio to your TV and movie-viewing experience, as well as making music really pop. Check out all nine of the available Sonos deals below.</p>        <div class="featured_product_block featured_block_hero" data-id="c0fff3fc-a5f6-11f1-8385-63c96ae4aca8">            <a href="https://www.bestbuy.com/product/sonos-era-300-smart-speaker-with-spatial-audio-each-black/J39H3736FZ/sku/6535189" data-model-name="Era 300 Smart Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/VzpyM36rK6kUAbhXX7PMQ.jpg" alt="Sonos, Sonos - Era 300 Smart Speaker With Spatial Audio (each) - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Era 300 Smart Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff55a-a5f6-11f1-9304-bdbd86e47408">            <a href="https://www.bestbuy.com/product/sonos-era-300-smart-speaker-with-spatial-audio-each-white/J39H3736F6/sku/6535188" data-model-name="Era 300 Smart Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/m4Fq6SgcCkR7A3S89MuRFK.jpg" alt="BRAND, Sonos - Era 300 Smart Speaker with Spatial Audio (Each) - White"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Era 300 Smart Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff730-a5f6-11f1-88a0-ffe4a7085132">            <a href="https://www.bestbuy.com/product/sonos-sub-mini-wireless-subwoofer-white/J39H373SPV/sku/6520133" data-model-name="Sub Mini Wireless Subwoofer" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/Ec4bU9AGPSXVpo2BsK4WBN.jpg" alt="Sonos, Sub Mini Wireless Subwoofer"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Sub Mini Wireless Subwoofer</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff5d2-a5f6-11f1-97bf-83785a68e0b1">            <a href="https://www.bestbuy.com/product/sonos-sub-mini-wireless-subwoofer-black/J39H373SS8/sku/6520132" data-model-name="Sub Mini Wireless Subwoofer" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/4iuCjKhcGC2sEjCvg2mDdP.jpg" alt="Sonos - Sub Mini Wireless Subwoofer - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Sub Mini Wireless Subwoofer</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff6c2-a5f6-11f1-aa6b-7dcc8bf87666">            <a href="https://www.bestbuy.com/product/sonos-beam-gen-2-5-0-channel-smart-soundbar-with-dolby-atmos-white/J39H373LTP/sku/6476040" data-model-name="Beam (gen 2) 5.0 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/XkZPCg9sw8dzAG5orXb8Za.jpg" alt="Sonos, Sonos - Beam (gen 2) 5.0 Channel Smart Soundbar With Dolby Atmos - White"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Beam (gen 2) 5.0 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff474-a5f6-11f1-ab7b-75b6e53fa6b8">            <a href="https://www.bestbuy.com/product/sonos-beam-gen-2-5-0-channel-smart-soundbar-with-dolby-atmos-black/J39H373LWC/sku/6476041" data-model-name="Beam (gen 2) 5.0 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/CUam9LquQFCwn233ofKeu8.jpg" alt="Sonos, Sonos - Beam (gen 2) 5.0 Channel Smart Soundbar With Dolby Atmos - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Beam (gen 2) 5.0 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff4e2-a5f6-11f1-95bf-a18395a8aeb5">            <a href="https://www.bestbuy.com/product/sonos-sub-4-wireless-subwoofer-black/J39H37879G/sku/6597168" data-model-name="Sub 4 Wireless Subwoofer" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/3goSFz7wqBbcLFLC3cjySC.jpg" alt="Sonos, Sonos - Sub 4 Wireless Subwoofer - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Sub 4 Wireless Subwoofer</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff37a-a5f6-11f1-add0-bb8878425934">            <a href="https://www.bestbuy.com/product/sonos-arc-ultra-9-1-4-channel-smart-soundbar-with-dolby-atmos-black/J39H373F2S/sku/6597167" data-model-name="Arc Ultra 9.1.4 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/X6ZBq26yRtkqRM3AaW23vh.jpg" alt="Sonos, Sonos - Arc Ultra 9.1.4 Channel Smart Soundbar With Dolby Atmos - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Arc Ultra 9.1.4 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c0fff64a-a5f6-11f1-9b27-6d4f8bb38486">            <a href="https://www.bestbuy.com/product/sonos-arc-ultra-9-1-4-channel-smart-soundbar-with-dolby-atmos-white/J39H373F2X/sku/6597166" data-model-name="Arc Ultra 9.1.4 Channel Smart Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/je9XmwWx7dgUwwiFYXwgoW.jpg" alt="BRAND, Sonos - Arc Ultra 9.1.4 Channel Smart Soundbar with Dolby Atmos - White"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sonos</div>                                        <div class="featured__title">Arc Ultra 9.1.4 Channel Smart Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tested the Sony Bravia Theater Bar 7 + subwoofer — and recommending it is a no-brainer ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Sony Bravia Theater Bar 7 is Sony’s mid-range soundbar with true Dolby Atmos and DTS Virtual:X. If you’ve got a compatible Bravia TV, you even get access to IMAX Enhanced sound. Of course, it’s also a stellar “smart”(ish) home product, with Wi-Fi or Bluetooth streaming from various music streaming services. </p><p>As typical with the Theater Bar line, you can get the 7 with or without the subwoofer. I tested the model with the subwoofer. If you want a Sony Bravia experience for less than $1,099, you could check out the $349 Sony Bravia Theater Bar 5, but this isn’t compatible with Sony’s wireless subs. </p><p>With crystal-clear dialogue, powerful bass, and impressive surround sound, could the Bravia Theater Bar 7 be one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> of 2026? You bet. For the full story, keep reading this Sony Bravia Theater Bar 7 review. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-specs"><span>Sony Bravia Theater Bar 7 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Sony-BRAVIA-Theater-Soundbar-Speakers/dp/B0GSC13M1K" target="_blank" rel="nofollow">$869-$1,099</a> / <a href="https://www.amazon.co.uk/Sony-Theatre-Soundbar-Speaker-Surround/dp/B0GSXFKCJ4/" target="_blank" rel="nofollow">£699-£948</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI 2.1, Bluetooth, Wi-Fi</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>Soundbar: 28.3 pounds / Subwoofer: 13.7 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>Soundbar: 5 x 37.5 x 2.6 inches / Subwoofer: 13.9 x 13.9 x 5.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>5.1.2 (with sub)</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-price-availability"><span>Sony Bravia Theater Bar 7 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="s83F4GUL5xZwgiG6PWsMNE" name="Sony Bravia Theater Bar 7 _ 4.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/s83F4GUL5xZwgiG6PWsMNE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Sony Bravia Theater Bar 7 + Sub is <a href="https://www.amazon.com/Sony-BRAVIA-Theater-Soundbar-Speakers/dp/B0GSC13M1K" target="_blank" rel="nofollow">$1,099 from Amazon U.S.</a> Without the sub, you’re looking at <a href="https://www.amazon.com/Sony-BRAVIA-Theater-Soundbar-Speakers/dp/B0GSBYNNX1" target="_blank" rel="nofollow">$869 from Amazon U.S.</a> I tested the subwoofer version, so that’s the model I’ll be discussing today.</p><p>In the U.K, the soundbar alone is <a href="https://www.amazon.co.uk/Sony-Theatre-Soundbar-Speaker-Surround/dp/B0GSXFKCJ4/" target="_blank" rel="nofollow">£699 from Amazon U.K.</a> You can’t buy the soundbar and subwoofer in one bundle in the U.K., but the overall price would be £948 — the sub is £249.</p><p>If you change your mind post-purchase and decide you want the subwoofer, you can buy it for <a href="https://www.amazon.com/Sony-Theater-Wireless-Subwoofer-Soundbar/dp/B0GSCFT5SG" target="_blank" rel="nofollow">$329</a>/£<a href="https://www.amazon.co.uk/Sony-BRAVIA-Theatre-Wireless-Subwoofer/dp/B0F24K6YDG" target="_blank" rel="nofollow">249</a> at Amazon. This subwoofer is compatible with: Bravia Theater 7, 8, and 9, as well as the Bravia Theater Trio and Quad, and the HT-A7000, HT-A5000, HT-A3000, and STR-AN1000 amps. Theoretically, you could get the Bravia Bar 7 and use the Subwoofer 9 ($898). Sony also offers the Theater Bar 5, 6, 8, and 9, which range from $349 to $1,498. </p><p>In comparison, this is a pretty average soundbar price. It’s not particularly good value or poor value. You can get the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs90h-review">Samsung QS90H</a>, an all-in-one soundbar (yes, including a built-in sub that’s actually powerful), for $999, or the <a href="https://www.tomsguide.com/audio/soundbars/bose-lifestyle-ultra-soundbar-hands-on-review-finally-a-real-sonos-arc-ultra-rival">Bose Lifestyle Ultra Soundbar</a> for $1,099. </p><p>If you want something a little cheaper but still good, I’d recommend the $449 <a href="https://www.tomsguide.com/audio/soundbars/polk-audio-signa-s4-review">Polk Audio Signa S4</a> or the $699 <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-design"><span>Sony Bravia Theater Bar 7 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="j5QAboXaM5xZJDn7L7kfgD" name="Sony Bravia Theater Bar 7 _ 9.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/j5QAboXaM5xZJDn7L7kfgD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Sony Bravia Theater Bar 7 is relatively compact, making it ideal for TVs 55 inches and under. I tested it with a 55-inch LG G6, and it looked divine. Like most high-end soundbars, the Theater Bar 7 is effectively a long rectangle covered in a fine dark gray mesh. There’s no logo on the front, only a soft white light on the bottom right. No, you can’t tell it’s an expensive Sony soundbar from one glance, but you don’t buy a soundbar for the branding. You buy it for the sound. </p><p>Thankfully, as you’ll soon discover, it sounds great. </p><h2 id="controls-2">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EXPL2DoBpfagMHFxmipJCE" name="Sony Bravia Theater Bar 7 _.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/EXPL2DoBpfagMHFxmipJCE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In terms of controls, there’s a physical remote with a few basic commands. You can increase or decrease volume, turn it on and off, and toggle night, sound field, and voice modes. Of course, you can also control inputs. </p><p>This is effectively the same as the phone app, but the phone app, of course, has more advanced settings, which I’ll get into soon.</p><h2 id="features">Features</h2><p>Like Samsung soundbars, most of the Theater Bar 7’s USPs reside within compatible Bravia TVs. Unfortunately, I had to test with an LG G6 (boo-hoo me, I got to use one of the <a href="https://www.tomsguide.com/us/best-tvs,review-2224.html">best TVs</a> of 2026, right?)</p><p>As any soundbar worth its salt should have, the Theater Bar 7 has a night mode and voice mode. Night mode reduces bass so you can watch blockbusters at 3am without receiving a noise complaint from your neighbors, while voice mode accentuates dialogue without making it sound unnatural. </p><p>There’s also a sound field mode. This effectively widens and raises the sound, which majorly improved my overall experience while watching movies.</p><p>The Bravia Connect app has a litany of advanced features, which I’ll dive into now. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-app"><span>Sony Bravia Theater Bar 7 review: App</span></h2><p>The Theater Bar 7 uses Sony’s Bravia Connect app. While the app looks incredibly basic, almost bordering on careless, it boasts a range of advanced features. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NAVNsWoHjoMWxX6ujWwHVV" name="sony bravia theater bar 7" alt="three screenshots from the sony connect app showing room configuration and advanced audio settings menus" src="https://cdn.mos.cms.futurecdn.net/NAVNsWoHjoMWxX6ujWwHVV.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / Sony)</span></figcaption></figure><p>As you can see, there’s a room calibration setting, much like on the JBL Bar 1300X MK2 and the Samsung QS90H. This sends out a pulse of sound and calibrates the audio based on your watching space. </p><p>You can also adjust the sound field height. I set mine to the max, which benefited the flying scenes in “Top Gun: Maverick” massively. </p><p>Of course, you can also play music directly through Spotify or Qobuz Connect. You can also turn on DSEE Ultimate, an AI-assisted upscaler. If you’re an audio nerd, there’s no shortage of fun things to play with in Bravia Connect.</p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-sound-quality"><span>Sony Bravia Theater Bar 7 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hLs9LrE5LFWtBUDAqeg4yD" name="Sony Bravia Theater Bar 7 _ 7.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/hLs9LrE5LFWtBUDAqeg4yD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I test every soundbar in the same room, with “Top Gun: Maverick” on 4K Blu-ray, various other Blu-rays, TV on basic Netflix, and music.</p><h2 id="movies">Movies</h2><p>As always, I started with “Top Gun: Maverick”. I customized the sound field height to max, upped the bass to 10, and turned on sound field and voice mode. The soundbar balanced in-scene sounds like jet engines with the soundtrack well. </p><p>However, some, but not all, high-frequency sounds were lost in the bass-heavy “Danger Zone” soundtrack. Despite this, louder details like whooshing steam and engine takeoff sounds were clear. Only the softer whipping of ropes melted into the background. </p><p>Even so, the bass on “Danger Zone” was excellent, and the vocals sauntered across the soundstage with unbelievable clarity. </p><p>In the first dog-fighting scene, I could feel the planes flying overhead and around me. The screeching jet engines rumbled through the soundbar without overpowering the soundtrack, which flitted underneath the fighter jets with grace. Even the radioed dialogue was perfectly audible. </p><p>Throughout the movie, I never struggled to hear dialogue, even in flying scenes. Every word was perfectly clear. Every note in the piano-led soundtrack twinkled across the soundscape like a beacon. Ocean waves and whistling wind sound effects were woven underneath dialogue beautifully. This was a fantastic viewing experience — just like the movie theater.  </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JvNxyKfCyq7o5Z6Y66kPLE" name="Sony Bravia Theater Bar 7 _ 5.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/JvNxyKfCyq7o5Z6Y66kPLE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Next, I watched “Michael”. I needed to evaluate how the soundbar handled both dialogue and in-movie music beyond the soundtrack. Again, dialogue was crystal clear; perhaps this is TMI for some, but I could even make out the sound of saliva in the actors’ mouths. </p><p>The soundbar handled this movie excellently. Every performance was rich and immersive, with powerful yet controlled bass and exceptional clarity across the guitars, percussion, and vocals. Switching back to quiet, introspective scenes felt natural. </p><p>Finally, I watched “Oppenheimer”. The sound effects during the space dust clips (I’m not sure how else to describe them) glimmered and glitched in a way that made me feel like I was floating in the aether. The string-heavy soundtrack took control of the scenes and held tight, making me feel like I was also a physicist spinning out of control. </p><p>The softer detail sound effects like chirping birds and quacking ducks, as well as the crinkling of fabrics brushing together, also came through strongly. </p><h2 id="tv">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BdPE2iGC8GihhSmSpukBpD" name="Sony Bravia Theater Bar 7 _ 8.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/BdPE2iGC8GihhSmSpukBpD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV performance, I watched “Interview with the Vampire” on basic Netflix, as you’re not always going to be watching 4K Blu-rays. </p><p>Even on basic Netflix, I could still hear soft in-scene sounds like the fluttering of wings or the subtle intake of breath. Dialogue, again, was clear and never overpowered the soundtrack. Action scenes, such as when Lestat transfixes his human prey in the speakeasy, were clear. </p><p>Diegetic (in-scene) sound effects like chirping crickets and crackling fires were audible over dialogue, no matter how softly spoken. </p><p>Overall, this performance was excellent. I really have no issues with this soundbar at all, except the <em>slight</em> muffling of high-frequency details in “Top Gun: Maverick”. </p><h2 id="music">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="iv7zpY65iEDLFhQHpAUxrD" name="Sony Bravia Theater Bar 7 _ 6.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/iv7zpY65iEDLFhQHpAUxrD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Music performance is okay. It’s neither the worst soundbar I’ve ever heard or the best — the best is still the Bose Smart Soundbar (2024). Vocals had a tendency to feel quite pinchy, and bass was strangely lackluster. But, I digress — soundbars aren’t made to play music. They’re designed to be the center of your home cinema.</p><p>I listened to “Knocking at the Sky” by The Last Dinner Party. While the soft “la la” notes right at the end sounded great, the bombastic chorus was muddled and the vocals pinched in my ears. There was a disappointing amount of tinniness in the percussion, too. </p><p>Electronic music fared a little better. “If you wanna party, come over to my house (Eli Escobar remix)” by Fcukers was clearer and didn’t hurt my ears. The pulsing synth and warbling synth details were both full-bodied and confident, but the bass synth fell flat for me. There just wasn’t any intensity, likely due to the lack of bass.</p><p>Lastly, I switched to something heavier: “Red40” by Die Spitz. This guitar-led metal-punk-indie track is usually propulsive and insistent, but the vocals were squashed into the guitars and the kick drum didn’t carry any weight. </p><p>I was disappointed by this music performance because of how utterly excellent the movie and TV experience was, but you can’t win them all, and this is a soundbar — not a speaker. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-verdict"><span>Sony Bravia Theater Bar 7 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="R22rbBhqHpLoTD4AYbR7QE" name="Sony Bravia Theater Bar 7 _ 3.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/R22rbBhqHpLoTD4AYbR7QE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Sony Bravia Theater Bar 7 is a fantastic soundbar. With the subwoofer, you get powerful, controlled, and immersive bass, and the soundbar itself has a knack for delivering crystal-clear dialogue balanced perfectly over top of a dramatic OST. In terms of movie and TV performance, I really have nothing bad to say. Other than the <em>tiny</em> suppression of some high-frequency sounds in “Top Gun: Maverick”, the Bar 7 gets applause all round from me. </p><p>Sure, the app could look a little nicer — it’s nothing compared to the aesthetic of SmartThings — but are you going to be spending that much time simply looking at it? It works well and is reliable, with zero connectivity issues, and for me, that’s enough. </p><p>Whether you buy the subwoofer or not, I’m sure you’ll be incredibly pleased with the Theater Bar 7. It’s reliable, powerful, and immersive. For me, that’s all I want from a soundbar. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/sony-bravia-theater-bar-7-review</link>
                                                                            <description>
                            <![CDATA[ The Sony Bravia Theater Bar 7 + subwoofer is an instant home theater upgrade, with crystal-clear dialogue, impressive bass, and music streaming. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">sdnkBh9Qm7shNhwQV22TWJ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/NaMzNekhTVBsYtbsSfJh6E-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 29 Aug 2026 05:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/NaMzNekhTVBsYtbsSfJh6E-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom&#039;s guide blue background]]></media:description>                                                            <media:text><![CDATA[the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom&#039;s guide blue background]]></media:text>
                                <media:title type="plain"><![CDATA[the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom&#039;s guide blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/NaMzNekhTVBsYtbsSfJh6E-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Sony Bravia Theater Bar 7 is Sony’s mid-range soundbar with true Dolby Atmos and DTS Virtual:X. If you’ve got a compatible Bravia TV, you even get access to IMAX Enhanced sound. Of course, it’s also a stellar “smart”(ish) home product, with Wi-Fi or Bluetooth streaming from various music streaming services. </p><p>As typical with the Theater Bar line, you can get the 7 with or without the subwoofer. I tested the model with the subwoofer. If you want a Sony Bravia experience for less than $1,099, you could check out the $349 Sony Bravia Theater Bar 5, but this isn’t compatible with Sony’s wireless subs. </p><p>With crystal-clear dialogue, powerful bass, and impressive surround sound, could the Bravia Theater Bar 7 be one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> of 2026? You bet. For the full story, keep reading this Sony Bravia Theater Bar 7 review. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-specs"><span>Sony Bravia Theater Bar 7 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Sony-BRAVIA-Theater-Soundbar-Speakers/dp/B0GSC13M1K" target="_blank" rel="nofollow">$869-$1,099</a> / <a href="https://www.amazon.co.uk/Sony-Theatre-Soundbar-Speaker-Surround/dp/B0GSXFKCJ4/" target="_blank" rel="nofollow">£699-£948</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI 2.1, Bluetooth, Wi-Fi</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>Soundbar: 28.3 pounds / Subwoofer: 13.7 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>Soundbar: 5 x 37.5 x 2.6 inches / Subwoofer: 13.9 x 13.9 x 5.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>5.1.2 (with sub)</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-price-availability"><span>Sony Bravia Theater Bar 7 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="s83F4GUL5xZwgiG6PWsMNE" name="Sony Bravia Theater Bar 7 _ 4.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/s83F4GUL5xZwgiG6PWsMNE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Sony Bravia Theater Bar 7 + Sub is <a href="https://www.amazon.com/Sony-BRAVIA-Theater-Soundbar-Speakers/dp/B0GSC13M1K" target="_blank" rel="nofollow">$1,099 from Amazon U.S.</a> Without the sub, you’re looking at <a href="https://www.amazon.com/Sony-BRAVIA-Theater-Soundbar-Speakers/dp/B0GSBYNNX1" target="_blank" rel="nofollow">$869 from Amazon U.S.</a> I tested the subwoofer version, so that’s the model I’ll be discussing today.</p><p>In the U.K, the soundbar alone is <a href="https://www.amazon.co.uk/Sony-Theatre-Soundbar-Speaker-Surround/dp/B0GSXFKCJ4/" target="_blank" rel="nofollow">£699 from Amazon U.K.</a> You can’t buy the soundbar and subwoofer in one bundle in the U.K., but the overall price would be £948 — the sub is £249.</p><p>If you change your mind post-purchase and decide you want the subwoofer, you can buy it for <a href="https://www.amazon.com/Sony-Theater-Wireless-Subwoofer-Soundbar/dp/B0GSCFT5SG" target="_blank" rel="nofollow">$329</a>/£<a href="https://www.amazon.co.uk/Sony-BRAVIA-Theatre-Wireless-Subwoofer/dp/B0F24K6YDG" target="_blank" rel="nofollow">249</a> at Amazon. This subwoofer is compatible with: Bravia Theater 7, 8, and 9, as well as the Bravia Theater Trio and Quad, and the HT-A7000, HT-A5000, HT-A3000, and STR-AN1000 amps. Theoretically, you could get the Bravia Bar 7 and use the Subwoofer 9 ($898). Sony also offers the Theater Bar 5, 6, 8, and 9, which range from $349 to $1,498. </p><p>In comparison, this is a pretty average soundbar price. It’s not particularly good value or poor value. You can get the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs90h-review">Samsung QS90H</a>, an all-in-one soundbar (yes, including a built-in sub that’s actually powerful), for $999, or the <a href="https://www.tomsguide.com/audio/soundbars/bose-lifestyle-ultra-soundbar-hands-on-review-finally-a-real-sonos-arc-ultra-rival">Bose Lifestyle Ultra Soundbar</a> for $1,099. </p><p>If you want something a little cheaper but still good, I’d recommend the $449 <a href="https://www.tomsguide.com/audio/soundbars/polk-audio-signa-s4-review">Polk Audio Signa S4</a> or the $699 <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-design"><span>Sony Bravia Theater Bar 7 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="j5QAboXaM5xZJDn7L7kfgD" name="Sony Bravia Theater Bar 7 _ 9.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/j5QAboXaM5xZJDn7L7kfgD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Sony Bravia Theater Bar 7 is relatively compact, making it ideal for TVs 55 inches and under. I tested it with a 55-inch LG G6, and it looked divine. Like most high-end soundbars, the Theater Bar 7 is effectively a long rectangle covered in a fine dark gray mesh. There’s no logo on the front, only a soft white light on the bottom right. No, you can’t tell it’s an expensive Sony soundbar from one glance, but you don’t buy a soundbar for the branding. You buy it for the sound. </p><p>Thankfully, as you’ll soon discover, it sounds great. </p><h2 id="controls-2">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EXPL2DoBpfagMHFxmipJCE" name="Sony Bravia Theater Bar 7 _.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/EXPL2DoBpfagMHFxmipJCE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In terms of controls, there’s a physical remote with a few basic commands. You can increase or decrease volume, turn it on and off, and toggle night, sound field, and voice modes. Of course, you can also control inputs. </p><p>This is effectively the same as the phone app, but the phone app, of course, has more advanced settings, which I’ll get into soon.</p><h2 id="features">Features</h2><p>Like Samsung soundbars, most of the Theater Bar 7’s USPs reside within compatible Bravia TVs. Unfortunately, I had to test with an LG G6 (boo-hoo me, I got to use one of the <a href="https://www.tomsguide.com/us/best-tvs,review-2224.html">best TVs</a> of 2026, right?)</p><p>As any soundbar worth its salt should have, the Theater Bar 7 has a night mode and voice mode. Night mode reduces bass so you can watch blockbusters at 3am without receiving a noise complaint from your neighbors, while voice mode accentuates dialogue without making it sound unnatural. </p><p>There’s also a sound field mode. This effectively widens and raises the sound, which majorly improved my overall experience while watching movies.</p><p>The Bravia Connect app has a litany of advanced features, which I’ll dive into now. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-app"><span>Sony Bravia Theater Bar 7 review: App</span></h2><p>The Theater Bar 7 uses Sony’s Bravia Connect app. While the app looks incredibly basic, almost bordering on careless, it boasts a range of advanced features. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NAVNsWoHjoMWxX6ujWwHVV" name="sony bravia theater bar 7" alt="three screenshots from the sony connect app showing room configuration and advanced audio settings menus" src="https://cdn.mos.cms.futurecdn.net/NAVNsWoHjoMWxX6ujWwHVV.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / Sony)</span></figcaption></figure><p>As you can see, there’s a room calibration setting, much like on the JBL Bar 1300X MK2 and the Samsung QS90H. This sends out a pulse of sound and calibrates the audio based on your watching space. </p><p>You can also adjust the sound field height. I set mine to the max, which benefited the flying scenes in “Top Gun: Maverick” massively. </p><p>Of course, you can also play music directly through Spotify or Qobuz Connect. You can also turn on DSEE Ultimate, an AI-assisted upscaler. If you’re an audio nerd, there’s no shortage of fun things to play with in Bravia Connect.</p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-sound-quality"><span>Sony Bravia Theater Bar 7 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hLs9LrE5LFWtBUDAqeg4yD" name="Sony Bravia Theater Bar 7 _ 7.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/hLs9LrE5LFWtBUDAqeg4yD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I test every soundbar in the same room, with “Top Gun: Maverick” on 4K Blu-ray, various other Blu-rays, TV on basic Netflix, and music.</p><h2 id="movies">Movies</h2><p>As always, I started with “Top Gun: Maverick”. I customized the sound field height to max, upped the bass to 10, and turned on sound field and voice mode. The soundbar balanced in-scene sounds like jet engines with the soundtrack well. </p><p>However, some, but not all, high-frequency sounds were lost in the bass-heavy “Danger Zone” soundtrack. Despite this, louder details like whooshing steam and engine takeoff sounds were clear. Only the softer whipping of ropes melted into the background. </p><p>Even so, the bass on “Danger Zone” was excellent, and the vocals sauntered across the soundstage with unbelievable clarity. </p><p>In the first dog-fighting scene, I could feel the planes flying overhead and around me. The screeching jet engines rumbled through the soundbar without overpowering the soundtrack, which flitted underneath the fighter jets with grace. Even the radioed dialogue was perfectly audible. </p><p>Throughout the movie, I never struggled to hear dialogue, even in flying scenes. Every word was perfectly clear. Every note in the piano-led soundtrack twinkled across the soundscape like a beacon. Ocean waves and whistling wind sound effects were woven underneath dialogue beautifully. This was a fantastic viewing experience — just like the movie theater.  </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JvNxyKfCyq7o5Z6Y66kPLE" name="Sony Bravia Theater Bar 7 _ 5.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/JvNxyKfCyq7o5Z6Y66kPLE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Next, I watched “Michael”. I needed to evaluate how the soundbar handled both dialogue and in-movie music beyond the soundtrack. Again, dialogue was crystal clear; perhaps this is TMI for some, but I could even make out the sound of saliva in the actors’ mouths. </p><p>The soundbar handled this movie excellently. Every performance was rich and immersive, with powerful yet controlled bass and exceptional clarity across the guitars, percussion, and vocals. Switching back to quiet, introspective scenes felt natural. </p><p>Finally, I watched “Oppenheimer”. The sound effects during the space dust clips (I’m not sure how else to describe them) glimmered and glitched in a way that made me feel like I was floating in the aether. The string-heavy soundtrack took control of the scenes and held tight, making me feel like I was also a physicist spinning out of control. </p><p>The softer detail sound effects like chirping birds and quacking ducks, as well as the crinkling of fabrics brushing together, also came through strongly. </p><h2 id="tv">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BdPE2iGC8GihhSmSpukBpD" name="Sony Bravia Theater Bar 7 _ 8.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/BdPE2iGC8GihhSmSpukBpD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV performance, I watched “Interview with the Vampire” on basic Netflix, as you’re not always going to be watching 4K Blu-rays. </p><p>Even on basic Netflix, I could still hear soft in-scene sounds like the fluttering of wings or the subtle intake of breath. Dialogue, again, was clear and never overpowered the soundtrack. Action scenes, such as when Lestat transfixes his human prey in the speakeasy, were clear. </p><p>Diegetic (in-scene) sound effects like chirping crickets and crackling fires were audible over dialogue, no matter how softly spoken. </p><p>Overall, this performance was excellent. I really have no issues with this soundbar at all, except the <em>slight</em> muffling of high-frequency details in “Top Gun: Maverick”. </p><h2 id="music">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="iv7zpY65iEDLFhQHpAUxrD" name="Sony Bravia Theater Bar 7 _ 6.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/iv7zpY65iEDLFhQHpAUxrD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Music performance is okay. It’s neither the worst soundbar I’ve ever heard or the best — the best is still the Bose Smart Soundbar (2024). Vocals had a tendency to feel quite pinchy, and bass was strangely lackluster. But, I digress — soundbars aren’t made to play music. They’re designed to be the center of your home cinema.</p><p>I listened to “Knocking at the Sky” by The Last Dinner Party. While the soft “la la” notes right at the end sounded great, the bombastic chorus was muddled and the vocals pinched in my ears. There was a disappointing amount of tinniness in the percussion, too. </p><p>Electronic music fared a little better. “If you wanna party, come over to my house (Eli Escobar remix)” by Fcukers was clearer and didn’t hurt my ears. The pulsing synth and warbling synth details were both full-bodied and confident, but the bass synth fell flat for me. There just wasn’t any intensity, likely due to the lack of bass.</p><p>Lastly, I switched to something heavier: “Red40” by Die Spitz. This guitar-led metal-punk-indie track is usually propulsive and insistent, but the vocals were squashed into the guitars and the kick drum didn’t carry any weight. </p><p>I was disappointed by this music performance because of how utterly excellent the movie and TV experience was, but you can’t win them all, and this is a soundbar — not a speaker. </p><h2 class="article-body__section" id="section-sony-bravia-theater-bar-7-review-verdict"><span>Sony Bravia Theater Bar 7 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="R22rbBhqHpLoTD4AYbR7QE" name="Sony Bravia Theater Bar 7 _ 3.JPG" alt="the sony bravia theater bar 7 photographed with the subwoofer with a TV on the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/R22rbBhqHpLoTD4AYbR7QE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Sony Bravia Theater Bar 7 is a fantastic soundbar. With the subwoofer, you get powerful, controlled, and immersive bass, and the soundbar itself has a knack for delivering crystal-clear dialogue balanced perfectly over top of a dramatic OST. In terms of movie and TV performance, I really have nothing bad to say. Other than the <em>tiny</em> suppression of some high-frequency sounds in “Top Gun: Maverick”, the Bar 7 gets applause all round from me. </p><p>Sure, the app could look a little nicer — it’s nothing compared to the aesthetic of SmartThings — but are you going to be spending that much time simply looking at it? It works well and is reliable, with zero connectivity issues, and for me, that’s enough. </p><p>Whether you buy the subwoofer or not, I’m sure you’ll be incredibly pleased with the Theater Bar 7. It’s reliable, powerful, and immersive. For me, that’s all I want from a soundbar. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Before you buy your next pair of speakers, make sure you know this one essential difference ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I wish I had bought myself better speakers growing up. For the better part of my late teens, I used a hand-me-down portable boombox that I used to play burned CDs from friends. It's not like these days where you can just pick up one of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> and be done with it.</p><p>Still, had I known about better audio products — and the differences between active and passive speakers — I would’ve traded out that boombox in a heartbeat. </p><p>So let’s break down the differences between speakers, starting with the most obvious distinction: How do speakers get power and information to play your favorite music?</p><h2 id="passive-vs-active-speakers-major-differences">Passive vs Active Speakers: Major differences</h2><p>If you’re just encountering these terms for the first time, the words “passive” and “active” might bring to mind two very different-sounding pairs of speakers — one pair that’s loud and aggressive, and another that’s quieter and muffled. </p><div><blockquote><p>Thankfully, 'passive' and 'active' doesn’t refer to how the speakers sound, but rather how their internal components work and whether or not they require external amplification.</p></blockquote></div><p>Thankfully, 'passive' and 'active' doesn’t refer to how the speakers sound, but rather how their internal components work and whether or not they require external amplification. That, in a nutshell, is the biggest difference between the two: Passive speakers require external amplification whereas active speakers come with an amplifier built in. </p><p>When it’s framed like this, you might immediately be drawn to active speakers — they sure sound like a lot less work — and you wouldn’t be wrong. </p><p>Since a manufacturer has done all the hard work of pairing the drivers to an amplifier, you don’t have to spend the time looking at specs sheets to make sure you’re not overdriving or under-driving them. However, by choosing to go with active speakers, you’re missing out on the secret advantage of passive speakers. (Hint: Changing out amplifiers at the drop of a hat is actually a benefit, not a curse.) </p><p>The other major difference is how these speakers actually get the sonic information passed over to them. On active speakers, information is obtained wirelessly, over optical audio or via a set of phono cables. Passive speakers use traditional speaker wire. We’ll get into the pros and cons of each in just a minute.</p><h2 id="active-speakers-pros-cons-and-sound-quality">Active speakers: Pros, cons and sound quality</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="D62CdZ8XFamJWG3nuVeXLa" name="Edifier_S880DB_MK2_.JPG" alt="the edifier s880db mkii speakers in white and wood colorway showing the connectivity options, remote control, against a blue background" src="https://cdn.mos.cms.futurecdn.net/D62CdZ8XFamJWG3nuVeXLa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The main advantage of active speakers is that they work right out of the box without any additional hardware. These days, most active speakers come with a number of wireless connectivity options built in like AirPlay, Chromecast Built-in, Spotify and Tidal Connect, alongside many more. </p><p>On the backside of the main speaker (we call this the lead speaker or, in some cases, the master speaker) are a plethora of ports. Here you’ll find your standard phono inputs, usually a sub-out if you want to connect a separate subwoofer, optical in and, if you’re lucky, an HDMI port that allows you to connect the speakers to your TV without an AV receiver.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SV6aDGVKbcC8HF7xKkgnKa" name="Edifier_S880DB_MK2_ 9.JPG" alt="the edifier s880db mkii speakers in white and wood colorway showing the connectivity options, remote control, against a blue background" src="https://cdn.mos.cms.futurecdn.net/SV6aDGVKbcC8HF7xKkgnKa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Active speakers are connectivity rich and easy to set up, so what’s the downside? The downside is that the speakers will always sound the same — which isn’t bad if you’re the kind of person who likes to set it and forget it. Tinkerers, however, won’t love this. </p><p>Sound quality will vary from speaker to speaker, obviously, and it's hard to give a broad sweeping generalization about the technology. However, if I had to give one, I'd say the majority of active speakers I've heard follow a v-shaped curve with big, bold bass response and sparkling highs. It's not that the mid-band frequencies are lost entirely; I just feel like they're less prominent in the active speakers I've owned.</p><h2 id="passive-speakers-pros-cons-and-sound-quality">Passive speakers: Pros, cons and sound quality</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6240px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="p8goaxn6ErRpXkDsfo57Q9" name="Ruark Sabre-R 16-9" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/p8goaxn6ErRpXkDsfo57Q9.jpg" mos="" align="middle" fullscreen="" width="6240" height="3512" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>This is going to sound like some wine-swilling non-sense, but different components impart different sonic signatures — and that includes speakers themselves. This comes down to a number of factors including the amplification technology, the digital-to-analog conversion and the DSP (digital signal processing), all of which vary from device to device. </p><p>When talking about speakers and their sound signatures, we sometimes shorthand this chain as tuning, and herein lies the true joy of buying passive speakers: You’re able to tinker the tuning to your exact specifications. In the audio world we have plenty of default tunings, but not everyone’s ears are shaped the same, nor are our music preferences the same. Being able to tailor the sound to your liking is a huge benefit for folks with specific preferences.</p><p>The bad news is that not all music components aren’t interchangeable — and it is possible to cause damage to your audio equipment by mismatching it. This is a lot of fun for some folks, and a very stressful situation for others.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5238px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Dhg5cqfikSKVRNqGEjGMV7" name="Ruark Sabre-R_5" alt="The Ruark Audio Sabre-R bookshelf speakers rear ports" src="https://cdn.mos.cms.futurecdn.net/Dhg5cqfikSKVRNqGEjGMV7.jpg" mos="" align="middle" fullscreen="" width="5238" height="2946" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>As far as sound quality is concerned, it will again vary between speakers and the components used to drive them. The difference between one system and another can be <em>extreme</em>. Still, I've found properly crafted passive systems to be more nuanced than their active counterparts. </p><p>When I listen to my passive speaker setup, I find new aural details I hadn't heard before in my favorite songs. (Billie Eilish's "<em>WHEN WE ALL FALL ASLEEP, WHERE DO WE GO?" </em>is filled with small details that are totally lost on some speaker systems.)</p><iframe allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" height="352" width="100%" id="" style="border-radius:12px" class="position-center" data-lazy-priority="low" data-lazy-src="https://open.spotify.com/embed/album/0S0KGZnfBGSIssfF54WSJh?utm_source=generator&si=4c53ca7c980248d7"></iframe><h2 id="passive-vs-active-speakers-recommended-models">Passive vs Active Speakers: Recommended models</h2><p>If you just want easy, plug-and-play speakers with lots of features built right in, you should go for a pair of active speakers. There are plenty to choose from, but some of my recent favorites are the affordable <a href="https://www.amazon.com/Edifier-S880DB-MKII-Bookshelf-Bluetooth/dp/B0FGNS3KFC/">Edifier S880DB MKII</a> and the pricier — but much more powerful — <a href="https://www.amazon.com/Klipsch-Nines-Powered-Speakers-Walnut/dp/B0GLQNLG39/">Klipsch The Nines II</a>. You can also go for floorstanding models like the <a href="https://www.amazon.com/Fluance-Floorstanding-Amplifier-Listening-Turntable/dp/B0B788RPVS/">Fluance Ai81 Elite</a>.</p><p>If you want to build out a modular system piecemeal to allow for upgrading down the road, then you want some good passive speakers. As mentioned earlier, picking out a pair is a lot harder because it will depend entirely on the receiver or amplifier you decide to pair them with. </p><p>If the idea sounds daunting, you can always buy a system in a box that pairs passive speakers with a receiver for you, like the <a href="https://www.amazon.com/Yamaha-YHT-4970U-5-1-Channel-Theater-System/dp/B0H2FRHKH5/" target="_blank" rel="nofollow">Yamaha YHT-4970U 5.1-Channel Home Theater System</a>. This is an affordable way to get started with home theater audio. </p><p>For music aficionados wanting a good starter package, pick up the <a href="https://www.amazon.com/Bluesound-Powernode-Multi-Room-Resolution-Streaming/dp/B0FHJ4YMDB/" target="_blank" rel="nofollow">Bluesound Powernode</a> and a pair of <a href="https://www.amazon.com/KEF-LS50-Meta-Carbon-Black/dp/B08G1T1CYZ/" target="_blank" rel="nofollow">KEF LS50 Meta</a> speakers. This way you’re getting both the modularity to upgrade down the line and the convenience of a streaming setup in one package.  </p><p>Recommending systems can be difficult as tastes vary so much from person to person, but building the reference system of your dreams is a lifelong pursuit that many folks — myself included — wholeheartedly enjoy.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/airpods/ios-27-beta-3-adds-a-clever-new-control-for-airpods-adaptive-mode"><strong>Apple's iOS 27 beta 3 update finally introduces a brilliantly clever and streamlined control interface for AirPods Adaptive mode, empowering users to instantly fine-tune ambient noise blending intensity with just a simple tap</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review"><strong>The Marshall Acton IV won me over with its charming retro design and incredible sound — and it’s earned a permanent spot on my coffee table</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review"><strong>‘Easy on the eyes and ridiculously stunning sound’ — I tested the Marshall Stanmore IV home speaker and it’s coming for Sonos’ crown</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/speakers/before-you-buy-your-next-pair-of-speakers-make-sure-you-know-this-one-essential-difference</link>
                                                                            <description>
                            <![CDATA[ Not sure whether you should buy passive or active speakers? We can help break down the differences. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">zHhEnAknBrP5dZdTM5eaKi</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/XTvp4PbNASrTXakCzLx76h-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 27 Aug 2026 08:15:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ Nick.Pino@futurenet.com (Nick Pino) ]]></author>                    <dc:creator><![CDATA[ Nick Pino ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/7xnnEdyK5eEbDVbS5pYB54.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nick Pino heads up the TV and AV verticals at Tom&#039;s Guide and covers everything from OLED TVs to the latest wireless headphones. He&#039;s a Level 1 Certified THX Home Theater Professional, and has attended classes on TV and audio calibration. On Tom&#039;s Guide, he looks after some of the site&#039;s important buying guides like the best TVs, best OLED TVs, best 4K TVs, so most of his day is spent watching and evaluating new screens from LG, Samsung, Sony, Hisense, TCL and Vizio. (And yes, he knows how lucky he is to say that.) He was formerly the Senior Editor, TV and AV at TechRadar (Tom&#039;s Guide&#039;s sister site) and has previously written for GamesRadar, Official Xbox Magazine, PC Gamer and other outlets over the last decade. He got his start on Best Buy&#039;s official video game magazine, @GAMER, for whom he attended his first E3 in 2013. He&#039;s served as a judge for CES&#039; prestigious Innovation Awards and has attended the tech conference in Las Vegas since 2014. He has a computer science degree from the University at Buffalo and still resides in the Buffalo area. (Go Bills!) Not sure which TV you should buy? Drop him an email or tweet him on Twitter and he can help you out.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/XTvp4PbNASrTXakCzLx76h-1280-80.jpg">
                                                            <media:credit><![CDATA[Klipsch/ KEF/ Tom’s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A pair of KEF and Klipsch speakers next to one another.]]></media:description>                                                            <media:text><![CDATA[A pair of KEF and Klipsch speakers next to one another.]]></media:text>
                                <media:title type="plain"><![CDATA[A pair of KEF and Klipsch speakers next to one another.]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/XTvp4PbNASrTXakCzLx76h-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I wish I had bought myself better speakers growing up. For the better part of my late teens, I used a hand-me-down portable boombox that I used to play burned CDs from friends. It's not like these days where you can just pick up one of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> and be done with it.</p><p>Still, had I known about better audio products — and the differences between active and passive speakers — I would’ve traded out that boombox in a heartbeat. </p><p>So let’s break down the differences between speakers, starting with the most obvious distinction: How do speakers get power and information to play your favorite music?</p><h2 id="passive-vs-active-speakers-major-differences">Passive vs Active Speakers: Major differences</h2><p>If you’re just encountering these terms for the first time, the words “passive” and “active” might bring to mind two very different-sounding pairs of speakers — one pair that’s loud and aggressive, and another that’s quieter and muffled. </p><div><blockquote><p>Thankfully, 'passive' and 'active' doesn’t refer to how the speakers sound, but rather how their internal components work and whether or not they require external amplification.</p></blockquote></div><p>Thankfully, 'passive' and 'active' doesn’t refer to how the speakers sound, but rather how their internal components work and whether or not they require external amplification. That, in a nutshell, is the biggest difference between the two: Passive speakers require external amplification whereas active speakers come with an amplifier built in. </p><p>When it’s framed like this, you might immediately be drawn to active speakers — they sure sound like a lot less work — and you wouldn’t be wrong. </p><p>Since a manufacturer has done all the hard work of pairing the drivers to an amplifier, you don’t have to spend the time looking at specs sheets to make sure you’re not overdriving or under-driving them. However, by choosing to go with active speakers, you’re missing out on the secret advantage of passive speakers. (Hint: Changing out amplifiers at the drop of a hat is actually a benefit, not a curse.) </p><p>The other major difference is how these speakers actually get the sonic information passed over to them. On active speakers, information is obtained wirelessly, over optical audio or via a set of phono cables. Passive speakers use traditional speaker wire. We’ll get into the pros and cons of each in just a minute.</p><h2 id="active-speakers-pros-cons-and-sound-quality">Active speakers: Pros, cons and sound quality</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="D62CdZ8XFamJWG3nuVeXLa" name="Edifier_S880DB_MK2_.JPG" alt="the edifier s880db mkii speakers in white and wood colorway showing the connectivity options, remote control, against a blue background" src="https://cdn.mos.cms.futurecdn.net/D62CdZ8XFamJWG3nuVeXLa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The main advantage of active speakers is that they work right out of the box without any additional hardware. These days, most active speakers come with a number of wireless connectivity options built in like AirPlay, Chromecast Built-in, Spotify and Tidal Connect, alongside many more. </p><p>On the backside of the main speaker (we call this the lead speaker or, in some cases, the master speaker) are a plethora of ports. Here you’ll find your standard phono inputs, usually a sub-out if you want to connect a separate subwoofer, optical in and, if you’re lucky, an HDMI port that allows you to connect the speakers to your TV without an AV receiver.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SV6aDGVKbcC8HF7xKkgnKa" name="Edifier_S880DB_MK2_ 9.JPG" alt="the edifier s880db mkii speakers in white and wood colorway showing the connectivity options, remote control, against a blue background" src="https://cdn.mos.cms.futurecdn.net/SV6aDGVKbcC8HF7xKkgnKa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Active speakers are connectivity rich and easy to set up, so what’s the downside? The downside is that the speakers will always sound the same — which isn’t bad if you’re the kind of person who likes to set it and forget it. Tinkerers, however, won’t love this. </p><p>Sound quality will vary from speaker to speaker, obviously, and it's hard to give a broad sweeping generalization about the technology. However, if I had to give one, I'd say the majority of active speakers I've heard follow a v-shaped curve with big, bold bass response and sparkling highs. It's not that the mid-band frequencies are lost entirely; I just feel like they're less prominent in the active speakers I've owned.</p><h2 id="passive-speakers-pros-cons-and-sound-quality">Passive speakers: Pros, cons and sound quality</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6240px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="p8goaxn6ErRpXkDsfo57Q9" name="Ruark Sabre-R 16-9" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/p8goaxn6ErRpXkDsfo57Q9.jpg" mos="" align="middle" fullscreen="" width="6240" height="3512" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>This is going to sound like some wine-swilling non-sense, but different components impart different sonic signatures — and that includes speakers themselves. This comes down to a number of factors including the amplification technology, the digital-to-analog conversion and the DSP (digital signal processing), all of which vary from device to device. </p><p>When talking about speakers and their sound signatures, we sometimes shorthand this chain as tuning, and herein lies the true joy of buying passive speakers: You’re able to tinker the tuning to your exact specifications. In the audio world we have plenty of default tunings, but not everyone’s ears are shaped the same, nor are our music preferences the same. Being able to tailor the sound to your liking is a huge benefit for folks with specific preferences.</p><p>The bad news is that not all music components aren’t interchangeable — and it is possible to cause damage to your audio equipment by mismatching it. This is a lot of fun for some folks, and a very stressful situation for others.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5238px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Dhg5cqfikSKVRNqGEjGMV7" name="Ruark Sabre-R_5" alt="The Ruark Audio Sabre-R bookshelf speakers rear ports" src="https://cdn.mos.cms.futurecdn.net/Dhg5cqfikSKVRNqGEjGMV7.jpg" mos="" align="middle" fullscreen="" width="5238" height="2946" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>As far as sound quality is concerned, it will again vary between speakers and the components used to drive them. The difference between one system and another can be <em>extreme</em>. Still, I've found properly crafted passive systems to be more nuanced than their active counterparts. </p><p>When I listen to my passive speaker setup, I find new aural details I hadn't heard before in my favorite songs. (Billie Eilish's "<em>WHEN WE ALL FALL ASLEEP, WHERE DO WE GO?" </em>is filled with small details that are totally lost on some speaker systems.)</p><iframe allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" height="352" width="100%" id="" style="border-radius:12px" class="position-center" data-lazy-priority="low" data-lazy-src="https://open.spotify.com/embed/album/0S0KGZnfBGSIssfF54WSJh?utm_source=generator&si=4c53ca7c980248d7"></iframe><h2 id="passive-vs-active-speakers-recommended-models">Passive vs Active Speakers: Recommended models</h2><p>If you just want easy, plug-and-play speakers with lots of features built right in, you should go for a pair of active speakers. There are plenty to choose from, but some of my recent favorites are the affordable <a href="https://www.amazon.com/Edifier-S880DB-MKII-Bookshelf-Bluetooth/dp/B0FGNS3KFC/">Edifier S880DB MKII</a> and the pricier — but much more powerful — <a href="https://www.amazon.com/Klipsch-Nines-Powered-Speakers-Walnut/dp/B0GLQNLG39/">Klipsch The Nines II</a>. You can also go for floorstanding models like the <a href="https://www.amazon.com/Fluance-Floorstanding-Amplifier-Listening-Turntable/dp/B0B788RPVS/">Fluance Ai81 Elite</a>.</p><p>If you want to build out a modular system piecemeal to allow for upgrading down the road, then you want some good passive speakers. As mentioned earlier, picking out a pair is a lot harder because it will depend entirely on the receiver or amplifier you decide to pair them with. </p><p>If the idea sounds daunting, you can always buy a system in a box that pairs passive speakers with a receiver for you, like the <a href="https://www.amazon.com/Yamaha-YHT-4970U-5-1-Channel-Theater-System/dp/B0H2FRHKH5/" target="_blank" rel="nofollow">Yamaha YHT-4970U 5.1-Channel Home Theater System</a>. This is an affordable way to get started with home theater audio. </p><p>For music aficionados wanting a good starter package, pick up the <a href="https://www.amazon.com/Bluesound-Powernode-Multi-Room-Resolution-Streaming/dp/B0FHJ4YMDB/" target="_blank" rel="nofollow">Bluesound Powernode</a> and a pair of <a href="https://www.amazon.com/KEF-LS50-Meta-Carbon-Black/dp/B08G1T1CYZ/" target="_blank" rel="nofollow">KEF LS50 Meta</a> speakers. This way you’re getting both the modularity to upgrade down the line and the convenience of a streaming setup in one package.  </p><p>Recommending systems can be difficult as tastes vary so much from person to person, but building the reference system of your dreams is a lifelong pursuit that many folks — myself included — wholeheartedly enjoy.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/airpods/ios-27-beta-3-adds-a-clever-new-control-for-airpods-adaptive-mode"><strong>Apple's iOS 27 beta 3 update finally introduces a brilliantly clever and streamlined control interface for AirPods Adaptive mode, empowering users to instantly fine-tune ambient noise blending intensity with just a simple tap</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review"><strong>The Marshall Acton IV won me over with its charming retro design and incredible sound — and it’s earned a permanent spot on my coffee table</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review"><strong>‘Easy on the eyes and ridiculously stunning sound’ — I tested the Marshall Stanmore IV home speaker and it’s coming for Sonos’ crown</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Amazon raised prices on Echo, Fire TV, and Kindle devices — here's how much more you're going to pay ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Over the weekend, Amazon quietly raised prices for nearly all of the devices the company makes, including the <a href="https://www.tomsguide.com/us/amazon-echo-dot-gen-3,review-5811.html">Echo Dot</a>, <a href="https://www.tomsguide.com/tablets/e-readers/kindle-paperwhite-2024-review">Kindle Paperwhite</a> and<a href="https://www.tomsguide.com/entertainment/streaming-devices/amazon-launches-updated-fire-tv-stick-hd-thats-slimmer-and-doesnt-require-a-wall-adapter"> Fire TV Stick HD</a>. It's the latest in a series of price increases that have hit nearly every company. You can guess what's to blame.</p><p>Amazon's new prices vary in amount, with at least one device seeing a 60% increase; the company cited the ongoing <a href="https://www.tomsguide.com/computing/the-ram-crisis-is-so-much-bigger-than-pcs-heres-the-rest-of-the-products-caught-in-the-crossfire">RAM crisis</a> and component inflation. First spotted by <a href="https://fortune.com/2026/08/21/exclusive-amazon-quietly-hiked-prices-echo-fire-tv-kindle-eero-significant-increases-memory-costs/" target="_blank">Fortune</a>, some of the company's cheaper devices appear to be the most affected. </p><p>Here's what the new prices look like:</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FYUSTSLJG4LM47i68vjPdg" name="Echo Show 21-5" alt="Watching Fire TV on the Echo Show 21 in the kitchen" src="https://cdn.mos.cms.futurecdn.net/FYUSTSLJG4LM47i68vjPdg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div ><table><caption>Smart speakers and displays</caption><thead><tr><th class="firstcol empty" ></th><th  ><p><strong>Original price</strong></p></th><th  ><p><strong>New price</strong></p></th><th  ><p><strong>Increase</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Echo Dot</strong></p></td><td  ><p>$49</p></td><td  ><p>$79</p></td><td  ><p>61.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Dot Max</strong></p></td><td  ><p>$99</p></td><td  ><p>$119</p></td><td  ><p>20.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Studio</strong></p></td><td  ><p>$219</p></td><td  ><p>$219</p></td><td  ><p>0%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 5</strong></p></td><td  ><p>$89</p></td><td  ><p>$99</p></td><td  ><p>11.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 8</strong></p></td><td  ><p>$179</p></td><td  ><p>$199</p></td><td  ><p>11.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 11</strong></p></td><td  ><p>$219</p></td><td  ><p>$249</p></td><td  ><p>13.7%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 15</strong></p></td><td  ><p>$299</p></td><td  ><p>$349</p></td><td  ><p>16.7%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 21</strong></p></td><td  ><p>$399</p></td><td  ><p>$499</p></td><td  ><p>25.1%</p></td></tr></tbody></table></div><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bqve9zMXpkbhMu5Ex5nCpb" name="Amazon Fire TV stick 4K max" alt="Amazon Fire TV stick 4K max" src="https://cdn.mos.cms.futurecdn.net/Bqve9zMXpkbhMu5Ex5nCpb.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div ><table><caption>Fire TV sticks</caption><thead><tr><th class="firstcol empty" ></th><th  ><p><strong>Original Price</strong></p></th><th  ><p><strong>New Price</strong></p></th><th  ><p><strong>Increase</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Amazon Fire TV Stick HD</strong></p></td><td  ><p>$35</p></td><td  ><p>$39</p></td><td  ><p>11.4%</p></td></tr><tr><td class="firstcol " ><p><strong>Fire TV Stick 4K Select</strong></p></td><td  ><p>$39</p></td><td  ><p>$49</p></td><td  ><p>25.6%</p></td></tr><tr><td class="firstcol " ><p><strong>Fire TV Stick 4K Max</strong></p></td><td  ><p>$59</p></td><td  ><p>$84</p></td><td  ><p>42.4%</p></td></tr></tbody></table></div><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="KVyvXAxLs7HEWJvCgxxEmM" name="Amazon Kindle Colorsoft-6.jpg" alt="Kindle Colorsoft" src="https://cdn.mos.cms.futurecdn.net/KVyvXAxLs7HEWJvCgxxEmM.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div ><table><caption>Kindles</caption><thead><tr><th class="firstcol empty" ></th><th  ><p><strong>Original Price</strong></p></th><th  ><p><strong>New Price</strong></p></th><th  ><p><strong>Increase</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Kindle</strong></p></td><td  ><p>$110</p></td><td  ><p>$149</p></td><td  ><p>35.5%</p></td></tr><tr><td class="firstcol " ><p><strong>Kindle Paperwhite</strong></p></td><td  ><p>$159</p></td><td  ><p>$199</p></td><td  ><p>25.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Kindle</strong> <strong>Colorsoft</strong></p></td><td  ><p>$249</p></td><td  ><p>$289</p></td><td  ><p>16.1%</p></td></tr></tbody></table></div><p>The Echo Dot saw the largest increase up 60% and $30, from $49 to $79, while the higher-tier Echo Show 11 is also getting a $30 hike from $219 to $249.</p><p>Additionally, the 16GB Kindle and Kindle Paperwhite saw hikes, going from $109 to $149 and $159 to $199, respectively. The Fire TV Stick HD only saw a $5 hike from $34 to $39 and the 4K Max rose from $59 to $84. </p><p>Surprisingly, the Amazon-owned <a href="https://www.tomsguide.com/face-off/ring-video-doorbell-3-vs-doorbell-pro">Ring lineup of doorbells and security cameras</a> didn't get more expensive. </p><p>An Amazon spokesperson told Tom's Guide, "After absorbing these increases for as long as we could, we recently adjusted pricing across our product lines." She noted that "significant" increases in memory and component costs led to the decision.</p><h2 id="still-a-cheaper-option">Still a cheaper option</h2><p>In some cases, despite the price hikes, Amazon's devices might still be the cheaper option. Unfortunately, that's because everything else also got more expensive this year. </p><p>In July, <a href="https://www.tomsguide.com/entertainment/streaming-devices/some-streaming-sticks-are-doubling-in-price-heres-whats-driving-up-costs">Roku bumped prices</a> on its streaming devices, with the Roku hitting $150 and the entry-level Roku Streaming Stick now at $40, up from $30.</p><p>Kindles might be more expensive than similar rival options like the Nook GlowLight 4, which comes with 32GB of memory for $149. </p><p>The Echo Dot, though, remains cheaper than the <a href="https://www.tomsguide.com/audio/smart-speakers/google-home-speaker-review">Google Home Speaker</a>, which starts at $99, though the gap is only $20 now. </p><h2 id="amazon-devices-outlook">Amazon Devices: Outlook</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="g4rv5xpNBNaytDaHXvb2nM" name="Echo Show 8 (4th Gen)--3" alt="The Echo Show 8 (4th Gen) with the Alexa+ logo on screen on a desk" src="https://cdn.mos.cms.futurecdn.net/g4rv5xpNBNaytDaHXvb2nM.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>At the moment, only the Fire tablets seem to be unaffected — in fact, they’re the only devices currently on sale, but this could be due largely to the fact that it’s <a href="https://www.tomsguide.com/news/ultimate-back-to-school-guide">back to school season</a>. </p><p>For years, Amazon’s Echo smart home speakers have been the company’s trojan horse to get its voice assistant in homes across the world. Given that <a href="https://www.tomsguide.com/ai/alexa-release-date-cost-devices-and-all-the-new-ai-features">Alexa+</a> is available on virtually every Amazon Echo device, the company may have calculated that it’s saturated the market enough so that adoption of Alexa+ wouldn’t be affected by any potential loss in sales.</p><p>Still, the timing is curious, as the company also announced last week that <a href="https://www.aboutamazon.com/news/devices/alexa-plus-fire-tv-free-ai">Alexa+ would be available, free of charge</a>, to its newest Fire TV devices. That means if you have a current-generation Amazon Fire TV Stick, the Fire TV Cube, or Amazon Ember smart TV, you can get the full version of Amazon’s AI-enabled assistant without needing a Prime account, or paying $19.99 per month. </p><p>Regardless of whether you want Alexa+, more than ever it’s worth waiting until Black Friday or Prime Day to purchase one of Amazon’s devices (sadly, the new sale price may be the old regular price). If you don’t want to wait that long, some third-party retailers are still offering the original price. For example, you can still find the Amazon Echo Dot for $49 at <a href="https://www.target.com/p/amazon-echo-dot-5th-gen-2022-smart-speaker-with-alexa-glacier-white/-/A-87130563" target="_blank" rel="nofollow">Target</a> and <a href="https://www.homedepot.com/pep/Amazon-Echo-Dot-5th-Gen-Smart-Speaker-with-Alexa-Deep-Sea-Blue-B09B93ZDG4/323260668" target="_blank" rel="nofollow">Home Depot</a>. </p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><strong></strong><a href="https://www.tomsguide.com/ai/my-12-year-old-noticed-a-missing-amazon-shopping-feature-so-i-tried-to-build-it-with-ai"><strong>My 12-year-old noticed a missing Amazon shopping feature — so I tried to build it with AI</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/sales-events/ive-had-amazon-prime-for-over-a-decade-heres-the-9-best-perks-youre-paying-for-but-probably-not-using"><strong>9 best Amazon Prime perks you're paying for but probably not using</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/sales-events/amazon-has-a-hidden-button-that-connects-you-with-a-real-customer-service-agent-heres-where-to-find-it"><strong>Amazon has a hidden button that connects you with a real customer service agent — here's where to find it</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/amazon-raised-prices-on-echo-fire-tv-and-kindle-devices-heres-how-much-more-youre-going-to-pay</link>
                                                                            <description>
                            <![CDATA[ This past weekend, Amazon raised prices on most of its devices, including the Echo, Kindle, Fire TV and eero. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">Js4G5GSp5o9jKtzyUmMBcA</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/wqVGnqkQWWiAV4PhBUSR8-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 24 Aug 2026 20:38:20 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[E-Readers]]></category>
                                                    <category><![CDATA[Routers]]></category>
                                                    <category><![CDATA[Streaming Devices]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Tablets]]></category>
                                                    <category><![CDATA[Computing Hardware]]></category>
                                                    <category><![CDATA[Entertainment]]></category>
                                                    <category><![CDATA[Streaming]]></category>
                                                                                                <author><![CDATA[ scott.younker@futurenet.com (Scott Younker) ]]></author>                    <dc:creator><![CDATA[ Scott Younker ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/RZsUpqcJ6Uj2q83oCUwNhQ.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Scott Younker is the West Coast Reporter at Tom’s Guide. He covers all the latest tech news, including phones, computing and more. He’s been involved in tech since 2011 covering everything from cameras and swimming pool equipment to the latest gaming consoles and smart TVs. He is on a seemingly never ending hunt to build the easiest to use home media system. &lt;/p&gt;&lt;p&gt;Before Tom’s Guide, Scott worked for publications like &lt;em&gt;Digital Trends, Outdoor Photographer, Dead Beats Panel&lt;/em&gt;, and in a brief detour, &lt;em&gt;America’s Funniest Home Videos&lt;/em&gt;. Yes, he has seen more pratfalls, silly pets and ridiculous home movie fails than is reasonably healthy. &lt;/p&gt;&lt;p&gt;When not writing about the latest devices or advances in chipsets, be sure to ask about Scott about disc golf and sustainability, or just if you’re being cheeky, ask about his noodle arm. If you truly want to get nerdy, bring up board games and his ongoing losing streak. &lt;/p&gt;&lt;p&gt;Scott joined Tom&#039;s Guide in 2024 as the West Coast Reporter. He graduated from the School of Journalism at the University of Oregon with a degree in magazine journalism and a minor in communications. While there he blogged or wrote for several magazines including the Fluxx, Ethos and the Oregon Commentator. He briefly wrote and managed a moderately successful blog focused on web comics. &lt;br&gt;Send secure tips on Signal: scyo1.55.&lt;/p&gt; ]]></dc:description>
                                                                                                        <dc:contributor><![CDATA[ Mike Prospero ]]></dc:contributor>
                                                                    <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/wqVGnqkQWWiAV4PhBUSR8-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Echo Dot with Clock review]]></media:description>                                                            <media:text><![CDATA[Echo Dot with Clock review]]></media:text>
                                <media:title type="plain"><![CDATA[Echo Dot with Clock review]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/wqVGnqkQWWiAV4PhBUSR8-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Over the weekend, Amazon quietly raised prices for nearly all of the devices the company makes, including the <a href="https://www.tomsguide.com/us/amazon-echo-dot-gen-3,review-5811.html">Echo Dot</a>, <a href="https://www.tomsguide.com/tablets/e-readers/kindle-paperwhite-2024-review">Kindle Paperwhite</a> and<a href="https://www.tomsguide.com/entertainment/streaming-devices/amazon-launches-updated-fire-tv-stick-hd-thats-slimmer-and-doesnt-require-a-wall-adapter"> Fire TV Stick HD</a>. It's the latest in a series of price increases that have hit nearly every company. You can guess what's to blame.</p><p>Amazon's new prices vary in amount, with at least one device seeing a 60% increase; the company cited the ongoing <a href="https://www.tomsguide.com/computing/the-ram-crisis-is-so-much-bigger-than-pcs-heres-the-rest-of-the-products-caught-in-the-crossfire">RAM crisis</a> and component inflation. First spotted by <a href="https://fortune.com/2026/08/21/exclusive-amazon-quietly-hiked-prices-echo-fire-tv-kindle-eero-significant-increases-memory-costs/" target="_blank">Fortune</a>, some of the company's cheaper devices appear to be the most affected. </p><p>Here's what the new prices look like:</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FYUSTSLJG4LM47i68vjPdg" name="Echo Show 21-5" alt="Watching Fire TV on the Echo Show 21 in the kitchen" src="https://cdn.mos.cms.futurecdn.net/FYUSTSLJG4LM47i68vjPdg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div ><table><caption>Smart speakers and displays</caption><thead><tr><th class="firstcol empty" ></th><th  ><p><strong>Original price</strong></p></th><th  ><p><strong>New price</strong></p></th><th  ><p><strong>Increase</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Echo Dot</strong></p></td><td  ><p>$49</p></td><td  ><p>$79</p></td><td  ><p>61.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Dot Max</strong></p></td><td  ><p>$99</p></td><td  ><p>$119</p></td><td  ><p>20.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Studio</strong></p></td><td  ><p>$219</p></td><td  ><p>$219</p></td><td  ><p>0%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 5</strong></p></td><td  ><p>$89</p></td><td  ><p>$99</p></td><td  ><p>11.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 8</strong></p></td><td  ><p>$179</p></td><td  ><p>$199</p></td><td  ><p>11.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 11</strong></p></td><td  ><p>$219</p></td><td  ><p>$249</p></td><td  ><p>13.7%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 15</strong></p></td><td  ><p>$299</p></td><td  ><p>$349</p></td><td  ><p>16.7%</p></td></tr><tr><td class="firstcol " ><p><strong>Echo Show 21</strong></p></td><td  ><p>$399</p></td><td  ><p>$499</p></td><td  ><p>25.1%</p></td></tr></tbody></table></div><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bqve9zMXpkbhMu5Ex5nCpb" name="Amazon Fire TV stick 4K max" alt="Amazon Fire TV stick 4K max" src="https://cdn.mos.cms.futurecdn.net/Bqve9zMXpkbhMu5Ex5nCpb.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div ><table><caption>Fire TV sticks</caption><thead><tr><th class="firstcol empty" ></th><th  ><p><strong>Original Price</strong></p></th><th  ><p><strong>New Price</strong></p></th><th  ><p><strong>Increase</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Amazon Fire TV Stick HD</strong></p></td><td  ><p>$35</p></td><td  ><p>$39</p></td><td  ><p>11.4%</p></td></tr><tr><td class="firstcol " ><p><strong>Fire TV Stick 4K Select</strong></p></td><td  ><p>$39</p></td><td  ><p>$49</p></td><td  ><p>25.6%</p></td></tr><tr><td class="firstcol " ><p><strong>Fire TV Stick 4K Max</strong></p></td><td  ><p>$59</p></td><td  ><p>$84</p></td><td  ><p>42.4%</p></td></tr></tbody></table></div><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="KVyvXAxLs7HEWJvCgxxEmM" name="Amazon Kindle Colorsoft-6.jpg" alt="Kindle Colorsoft" src="https://cdn.mos.cms.futurecdn.net/KVyvXAxLs7HEWJvCgxxEmM.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div ><table><caption>Kindles</caption><thead><tr><th class="firstcol empty" ></th><th  ><p><strong>Original Price</strong></p></th><th  ><p><strong>New Price</strong></p></th><th  ><p><strong>Increase</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Kindle</strong></p></td><td  ><p>$110</p></td><td  ><p>$149</p></td><td  ><p>35.5%</p></td></tr><tr><td class="firstcol " ><p><strong>Kindle Paperwhite</strong></p></td><td  ><p>$159</p></td><td  ><p>$199</p></td><td  ><p>25.2%</p></td></tr><tr><td class="firstcol " ><p><strong>Kindle</strong> <strong>Colorsoft</strong></p></td><td  ><p>$249</p></td><td  ><p>$289</p></td><td  ><p>16.1%</p></td></tr></tbody></table></div><p>The Echo Dot saw the largest increase up 60% and $30, from $49 to $79, while the higher-tier Echo Show 11 is also getting a $30 hike from $219 to $249.</p><p>Additionally, the 16GB Kindle and Kindle Paperwhite saw hikes, going from $109 to $149 and $159 to $199, respectively. The Fire TV Stick HD only saw a $5 hike from $34 to $39 and the 4K Max rose from $59 to $84. </p><p>Surprisingly, the Amazon-owned <a href="https://www.tomsguide.com/face-off/ring-video-doorbell-3-vs-doorbell-pro">Ring lineup of doorbells and security cameras</a> didn't get more expensive. </p><p>An Amazon spokesperson told Tom's Guide, "After absorbing these increases for as long as we could, we recently adjusted pricing across our product lines." She noted that "significant" increases in memory and component costs led to the decision.</p><h2 id="still-a-cheaper-option">Still a cheaper option</h2><p>In some cases, despite the price hikes, Amazon's devices might still be the cheaper option. Unfortunately, that's because everything else also got more expensive this year. </p><p>In July, <a href="https://www.tomsguide.com/entertainment/streaming-devices/some-streaming-sticks-are-doubling-in-price-heres-whats-driving-up-costs">Roku bumped prices</a> on its streaming devices, with the Roku hitting $150 and the entry-level Roku Streaming Stick now at $40, up from $30.</p><p>Kindles might be more expensive than similar rival options like the Nook GlowLight 4, which comes with 32GB of memory for $149. </p><p>The Echo Dot, though, remains cheaper than the <a href="https://www.tomsguide.com/audio/smart-speakers/google-home-speaker-review">Google Home Speaker</a>, which starts at $99, though the gap is only $20 now. </p><h2 id="amazon-devices-outlook">Amazon Devices: Outlook</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="g4rv5xpNBNaytDaHXvb2nM" name="Echo Show 8 (4th Gen)--3" alt="The Echo Show 8 (4th Gen) with the Alexa+ logo on screen on a desk" src="https://cdn.mos.cms.futurecdn.net/g4rv5xpNBNaytDaHXvb2nM.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>At the moment, only the Fire tablets seem to be unaffected — in fact, they’re the only devices currently on sale, but this could be due largely to the fact that it’s <a href="https://www.tomsguide.com/news/ultimate-back-to-school-guide">back to school season</a>. </p><p>For years, Amazon’s Echo smart home speakers have been the company’s trojan horse to get its voice assistant in homes across the world. Given that <a href="https://www.tomsguide.com/ai/alexa-release-date-cost-devices-and-all-the-new-ai-features">Alexa+</a> is available on virtually every Amazon Echo device, the company may have calculated that it’s saturated the market enough so that adoption of Alexa+ wouldn’t be affected by any potential loss in sales.</p><p>Still, the timing is curious, as the company also announced last week that <a href="https://www.aboutamazon.com/news/devices/alexa-plus-fire-tv-free-ai">Alexa+ would be available, free of charge</a>, to its newest Fire TV devices. That means if you have a current-generation Amazon Fire TV Stick, the Fire TV Cube, or Amazon Ember smart TV, you can get the full version of Amazon’s AI-enabled assistant without needing a Prime account, or paying $19.99 per month. </p><p>Regardless of whether you want Alexa+, more than ever it’s worth waiting until Black Friday or Prime Day to purchase one of Amazon’s devices (sadly, the new sale price may be the old regular price). If you don’t want to wait that long, some third-party retailers are still offering the original price. For example, you can still find the Amazon Echo Dot for $49 at <a href="https://www.target.com/p/amazon-echo-dot-5th-gen-2022-smart-speaker-with-alexa-glacier-white/-/A-87130563" target="_blank" rel="nofollow">Target</a> and <a href="https://www.homedepot.com/pep/Amazon-Echo-Dot-5th-Gen-Smart-Speaker-with-Alexa-Deep-Sea-Blue-B09B93ZDG4/323260668" target="_blank" rel="nofollow">Home Depot</a>. </p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><strong></strong><a href="https://www.tomsguide.com/ai/my-12-year-old-noticed-a-missing-amazon-shopping-feature-so-i-tried-to-build-it-with-ai"><strong>My 12-year-old noticed a missing Amazon shopping feature — so I tried to build it with AI</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/sales-events/ive-had-amazon-prime-for-over-a-decade-heres-the-9-best-perks-youre-paying-for-but-probably-not-using"><strong>9 best Amazon Prime perks you're paying for but probably not using</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/sales-events/amazon-has-a-hidden-button-that-connects-you-with-a-real-customer-service-agent-heres-where-to-find-it"><strong>Amazon has a hidden button that connects you with a real customer service agent — here's where to find it</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ With its suave looks, class-leading battery life and phenomenal sound, the Marshall Stockwell III has become my go-to Bluetooth speaker ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Marshall has been on a roll in 2026, adding to its lineups of headphones, home speakers, and now, portable Bluetooth speakers. For the past couple of weeks, I’ve been testing the Marshall Stockwell III which succeeds the seven-year-old Stockwell II — and it has quickly become my go-to speaker for almost everything.</p><p>The Stockwell III improves on its predecessor by doubling the battery life (over 40 hours now), being more waterproof thanks to its IP55 rating, and delivering punchier bass, brighter treble, and incredible 360° sound. Though a little expensive and slightly heavy for a portable speaker, those feel like minor issues in an otherwise compelling (and beautiful) package.</p><p>So, is this the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best portable Bluetooth speaker</a> for you? Read my full Marshall Stockwell III review to find out.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-specs"><span>Marshall Stockwell III review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.marshall.com/gb/en/product/stockwell-iii" target="_blank" rel="nofollow">$249 / £199</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>IP55</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3 with SBC, AAC and LC3, 3.5mm and USB-C wired</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>1x 65W Class D amplifier (3-inch woofer), 2x 31W Class D amplifiers (two 1.75-inch wide-band drivers)</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>54Hz-20KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>10m / 32.8ft</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>40 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>7.1 x 6.4 x 2.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.9lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black and Brass, Cream</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-price-availability"><span>Marshall Stockwell III review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aEjTrwiHyg39dANWmMDziJ" name="Marshall_Stockwell_III_Speaker_010.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/aEjTrwiHyg39dANWmMDziJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III was first announced on 9 June, 2026, over seven years after its predecessor, the Marshall Stockwell II. Available to buy from 18 August, the Stockwell III has a retail price of <a href="https://www.marshall.com/gb/en/product/stockwell-iii" target="_blank" rel="nofollow">$249 / £199</a> — same as the second-gen model at launch in 2019. The Stockwell III comes in Black or Cream, and both have brass accents.</p><p>This is a premium portable Bluetooth speaker, and it finds itself competing with the likes of the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ultimate-ears-everboom-review">Ultimate Ears Everboom</a> ($249) and the <a href="https://www.tomsguide.com/audio/smart-speakers/sonos-play-review">Sonos Play</a> ($299). Though a little expensive, it’s still cheaper than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/i-just-tested-this-portable-bluetooth-speaker-and-it-could-last-you-forever">Bang & Olufsen Beosound A1 3rd Gen</a> which costs a whopping $399. The Stockwell III is worth the investment if you want 40+ hours of battery and beautiful sound stuffed into a compact (and stunning) package.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-design"><span>Marshall Stockwell III review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PRJYvqsoqRKGpUWrmCbzaK" name="Marshall_Stockwell_III_Speaker_005.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/PRJYvqsoqRKGpUWrmCbzaK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III is, in a nutshell, a ridiculously sexy speaker, and <em>the </em>portable speaker to get if you care about aesthetics above all else. Not much has changed between the Stockwell III and its predecessor when it comes to portability. The third-gen model measures 7.1 x 6.4 x 2.9 inches and weighs 2.9lbs. To make it easier to carry around, Marshall has included a guitar-esque, velvet carry strap in the box, which can be attached to the Stockwell III’s sides.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XNVzTW39bgryrU2HX2EoZK" name="Marshall_Stockwell_III_Speaker_012.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/XNVzTW39bgryrU2HX2EoZK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Given its hefty weight, the Stockwell III isn’t the most portable speaker around. The Ultimate Ears Everboom is lighter at 2.1lbs, as is the Bang & Olufsen Beosound A1 3rd Gen which weighs 1.27lbs. The Stockwell III’s strap makes it slightly easier to carry around, but if you want an even lighter speaker but with Marshall’s looks, the <a href="https://www.tomsguide.com/audio/marshal-emberton-iii-review">Emberton III</a> ($149, 1.5lbs) is a standout choice.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3009px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="99LhosA5GaBQhamtjfA9oC" name="Marshall_Stockwell_III-2" alt="Marshall Stockwell III Bluetooth portable speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/99LhosA5GaBQhamtjfA9oC.jpg" mos="" align="middle" fullscreen="" width="3009" height="1693" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Stockwell III looks more premium compared to its predecessor, too. While the Stockwell II featured white accents, the Stockwell III now features brass branding (on either black or cream), keeping in line with the brand’s other current-gen speakers — and headphones, too, like the <a href="https://www.tomsguide.com/audio/headphones/marshall-milton-anc-review">Milton ANC</a> ($229) and the <a href="https://www.tomsguide.com/audio/headphones/marshall-monitor-iii-review">Monitor III </a>($349). I much prefer this look as the black/cream and gold combo makes the speaker stand out in a crowded market.</p><p>Build quality is solid, thanks to metal mesh grilles and the textured silicone outer sleeve. You’re getting bang for your buck with the Stockwell III.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LfwoDyV3HgiQB9RGxD3vYK" name="Marshall_Stockwell_III_Speaker_016.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/LfwoDyV3HgiQB9RGxD3vYK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>But why should you buy the Stockwell III instead of Marshall’s other more portable (ergo, lightweight) speakers? The Stockwell III is a mid-sized, premium Bluetooth speaker that’s clearly designed for both home (given the Placement Compensation feature within the app) and light outdoor use (given its IP55 rating).</p><p>It’s got a more luxurious feel to it than the ultra-portable Emberton III, for instance, but it’s smaller than a massive party speaker, like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Bromley 450</a> ($799). If you want a small speaker to travel with, and one that can withstand the elements, get the IP67-rated Emberton III. But if you want a mid-sized speaker that isn’t as expensive or bulky as the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review">Kilburn III </a>($379), get the Stockwell III.</p><h2 id="weatherproofing">Weatherproofing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kdmxA8EAKBuqKA2pnxB6nC" name="Marshall_Stockwell_III-3" alt="Marshall Stockwell III Bluetooth portable speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/kdmxA8EAKBuqKA2pnxB6nC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Marshall has made significant improvements to the Stockwell III’s durability as the third-gen model is now IP55-rated — the Stockwell II was only IPX4-rated. IP55 means the Stockwell III is protected from dust and is fairly water resistant, though you can’t submerge it underwater like you can the IP67-rated Ultimate Ears Everboom.</p><p>IP55 should suffice for most scenarios, but be mindful of using it in wet conditions repeatedly as any gadget’s waterproofing wears down over time.</p><h2 id="controls-3">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yYPayj7Q9yfiEWByt7xCaK" name="Marshall_Stockwell_III_Speaker_014.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/yYPayj7Q9yfiEWByt7xCaK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>All the buttons and dials are located on the Stockwell III’s top plate, and you’ll be familiar with them if you’ve used any of Marshall’s other speakers, like the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review">Acton IV</a> ($299). Even if you aren’t, getting to grips with the controls is quick and easy. All the buttons and dials give great tactile feedback when pressed or turned. </p><p>There’s a Source button for swapping between Bluetooth and AUX, and a customizable ‘M’ button that, by default, activates Spotify Tap so you can start playing music without opening the app on your phone; playback joystick; and three amp-like knobs for adjusting volume, treble and bass. Alongside these, Marshall has also added a handy battery indicator.</p><h2 id="connectivity-2">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JmLp57pYSrVCXwTN2mbKaK" name="Marshall_Stockwell_III_Speaker_008.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/JmLp57pYSrVCXwTN2mbKaK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Stockwell III features a 3.5mm jack and a USB-C port for wired playback, as well as Bluetooth 5.3 for wireless connectivity. Over Bluetooth, you get support for SBC, AAC and LC3 codecs — the latter delivers high efficiency and better sound quality at low bitrates. This also gives it a leg-up over the Ultimate Ears Everboom which supports SBC and AAC only.</p><p>While the Stockwell II featured traditional multi-speaker pairing with other Stockwell IIs, the Stockwell III is Auracast-enabled — and that’s better as it enables multiple devices to connect to a single audio source without pairing. The third-gen model utilizes Bluetooth LE to essentially connect to an unlimited number of other Auracast-enabled Marshall speakers, such as the Emberton III or other Stockwell IIIs.</p><p>You also get multipoint connectivity, which means that you can connect two different Bluetooth devices to the Stockwell III simultaneously. I did this with my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> and <a href="https://www.tomsguide.com/phones/google-pixel-phones/google-pixel-10-pro-xl-review">Google Pixel 10 Pro XL</a> and it worked seamlessly.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-sound-quality"><span>Marshall Stockwell III review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="cJuZ93UVeGvdiFfv48yRVJ" name="Marshall_Stockwell_III_Speaker_011.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/cJuZ93UVeGvdiFfv48yRVJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III is very loud for its size — it’s one of the best-sounding portable Bluetooth speakers around. Under the hood, the Stockwell III is fitted with a 65W Class D amplifier to power its 3-inch woofer, and two 31W Class D amplifiers for its dual 1.75-inch wide-band drivers (versus two 5W tweeters and a single 10W woofer on the Stockwell II). The speaker covers a frequency response range of 54Hz to 20KHz.</p><p>Marshall has clearly spared no expense when it comes to stuffing the Stockwell III to the brim — and the result is simply outstanding sound. To test the speaker, I listened to a range of songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for audiophiles. ‘<a href="https://open.qobuz.com/track/398890244" target="_blank" rel="nofollow">2515</a>’ by Wasia Project, which has a strong, pulsating bassline acting as the track’s backbone throughout, sounded sublime. The song’s dark electronic pulse came through with real weight, while the bass remained tight and controlled, and never became muddy — even at high volumes.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4ygwMmfKZdmg5ZcjJJTdZK" name="Marshall_Stockwell_III_Speaker_006.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/4ygwMmfKZdmg5ZcjJJTdZK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This is where Marshall’s ‘Dynamic Loudness’ comes into play. The Stockwell III borrows this from the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Stanmore IV</a> ($399) and the Acton IV, and it basically ensures that sound remains balanced and distortion-free regardless of the volume. Playing ‘2515’ at 80% volume didn’t overwhelm the speaker, as the lows maintained their warmth and richness.</p><p>I then listened to ‘<a href="https://open.qobuz.com/track/64405603" target="_blank" rel="nofollow">Mariners Apartment Complex</a>’ by Lana Del Rey to test the soundstage — and sure enough, the Stockwell III didn’t falter. Lana’s vocals sat front and center, with her words remaining clear and never bleeding into one another. There was a great balance between her remarkable vocals and the acoustic guitar and piano, and the strings sounded textured and defined. Also, the True Stereophonic 360° — found on the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Bromley 750</a> ($1,299) and Bromley 450 party speakers — filled the room and the song sounded consistent when I moved around, giving the track an effortless sense of space and depth.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eBSeskbUXDHVeK2sMS28XK" name="Marshall_Stockwell_III_Speaker_007.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/eBSeskbUXDHVeK2sMS28XK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Lastly, I played ‘<a href="https://open.qobuz.com/track/76767860" target="_blank" rel="nofollow">Papua New Guinea</a>’ by The Future Sound of London, an electronic, ambient techno track. The driving basslines, swirling synths, breakbeat-style percussion and oscillating keyboard loops all came together to create a layered production, and the Stockwell III reproduced the dreamy nature extremely well. The treble felt vibrant and nuanced without becoming fatiguing. My partner and I couldn’t help but move along to the song, like we were at a rave.</p><p><em>Listen along to my review with the Marshall Stockwell III testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/68631732?zone=GB-en"></iframe><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-app"><span>Marshall Stockwell III review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="e2sBa5XjBvQ7LiP4hrBGMK" name="Marshall_Stockwell_III_Speaker_013.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/e2sBa5XjBvQ7LiP4hrBGMK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III is accompanied by the <a href="https://www.marshall.com/gb/en/support/speakers/learn/app-marshall-bluetooth" target="_blank" rel="nofollow">Marshall Bluetooth app</a>, available for free on iOS and Android. It’s the same app used by the British brand’s headphones and earbuds, and it’s your one-stop shop for adjusting the EQ or choosing a preset, updating the firmware, enabling Auracast, and customizing the ‘M’ button. The app sports a clean interface and is extremely easy to use.</p><p>You also get Placement Compensation, where you can select whether the speaker is close to an edge or corner, or flush against a wall. Once you’ve chosen your desired settings, the Stockwell III then corrects itself for any nearby reflective surfaces that may affect the sound quality. This feature has been borrowed from the Stanmore IV and Acton IV, which are plug-in home speakers. It’s handy to have if you’re keeping the Stanmore III in one place but given that it’s a portable Bluetooth speaker, I highly doubt you’ll be keeping it stationary.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/3FaDrwhoRqRafhHtBRDbtD.jpg" alt="Marshall Bluetooth app screenshots" /><figcaption><small role="credit">Marshall / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/MTA84CGuSfcTvezwcHYxxD.jpg" alt="Marshall Bluetooth app screenshots" /><figcaption><small role="credit">Marshall / Tom's Guide</small></figcaption></figure></figure><p>There’s a Source Mix option, too, which is new. It basically enables you to play music over Bluetooth <em>and </em>via USB-C at the same time. I can’t really think of a use case for this, but I suppose if you want to feel like a DJ, you can.</p><p>One thing I find surprising, though, is that the Stockwell III uses the Marshall Bluetooth app and not the new <a href="https://play.google.com/store/apps/details?id=com.marshallgroup.marshall&hl=en_GB" target="_blank" rel="nofollow">Marshall</a> app (came out in 2026 before the Stockwell III) which its fourth-gen home speakers utilize. Wouldn’t it make sense to have all of its speakers in one app? I’m personally not a fan of having two separate apps for different devices. Maybe it’s because the Stockwell III was actually created before the Stanmore IV and Acton IV were. Perhaps this is something Marshall will fix later. This isn’t a complaint, anyway, just an observation.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-battery-life"><span>Marshall Stockwell III review: Battery life</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:960px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aEjTrwiHyg39dANWmMDziJ" name="Marshall_Stockwell_III_Speaker_010.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/v2/t:108,l:439,cw:960,ch:540,q:80/aEjTrwiHyg39dANWmMDziJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Marshall has fitted a bigger battery into the Stockwell III, and the speaker can now run for over 40 hours on a single charge. That’s double the Stockwell II’s battery life. The Stockwell III trumps its competition too, as the Sonos Play is rated for 24 hours, and the Ultimate Ears Everboom is rated for 20. Even the pricier Bang & Olufsen Beosound A1 3rd Gen is rated for just 24 hours.</p><p>In my testing, I played at 50% volume over multiple listening sessions. The total time it took for the Stockwell III to run out of battery was just over 40 hours, keeping in line with Marshall’s claims.</p><p>Similar to the Sonos Play, the Stockwell III doubles as a power bank and you can use a USB-C cable to charge your smartphone, laptop and other devices. This is a great feature to have in a portable speaker as you don’t need to worry about finding a socket to charge your phone if you’re, say, out on a hike.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-verdict"><span>Marshall Stockwell III review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XCL39H5yGe7zmJRd3sMxXK" name="Marshall_Stockwell_III_Speaker_002.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/XCL39H5yGe7zmJRd3sMxXK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Just when I thought Marshall speakers couldn’t get any better, the iconic amp manufacturer comes out with something like the Stockwell III. This is an excellent portable speaker that delivers far more sound than its compact design suggests. Though it isn’t cheap — just shy of $250 — its combination of rich bass, vibrant treble and clear mids make a strong case for the price.</p><p>The Stockwell III also packs 40 hours of battery life, IP55 durability, and immersive 360° sound into a slim profile. While it’s still not the lightest portable speaker around, the included velvet strap makes it easier to carry around.</p><p>I can’t help but describe the speaker’s sound as genuinely astounding. If I had to take one speaker with me on my travels or to picnics outdoors, it would be the Stockwell III. At the time of writing, it’s Marshall’s best portable Bluetooth speaker right now.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/marshall-stockwell-iii-review</link>
                                                                            <description>
                            <![CDATA[ The Marshall Stockwell III is a beautiful portable speaker that can get very loud, and it boasts meaty bass and bright treble. It also packs a 40-hour battery. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">xwoMoRxtdVoyRuYBP9aMQM</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/9bzmRUT3eRzPgjBmwSYNgC-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 18 Aug 2026 06:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/9bzmRUT3eRzPgjBmwSYNgC-1280-80.jpg">
                                                            <media:credit><![CDATA[Nikita Achanta / Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Marshall Stockwell III Bluetooth portable speaker in black and brass]]></media:description>                                                            <media:text><![CDATA[Marshall Stockwell III Bluetooth portable speaker in black and brass]]></media:text>
                                <media:title type="plain"><![CDATA[Marshall Stockwell III Bluetooth portable speaker in black and brass]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/9bzmRUT3eRzPgjBmwSYNgC-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Marshall has been on a roll in 2026, adding to its lineups of headphones, home speakers, and now, portable Bluetooth speakers. For the past couple of weeks, I’ve been testing the Marshall Stockwell III which succeeds the seven-year-old Stockwell II — and it has quickly become my go-to speaker for almost everything.</p><p>The Stockwell III improves on its predecessor by doubling the battery life (over 40 hours now), being more waterproof thanks to its IP55 rating, and delivering punchier bass, brighter treble, and incredible 360° sound. Though a little expensive and slightly heavy for a portable speaker, those feel like minor issues in an otherwise compelling (and beautiful) package.</p><p>So, is this the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best portable Bluetooth speaker</a> for you? Read my full Marshall Stockwell III review to find out.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-specs"><span>Marshall Stockwell III review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.marshall.com/gb/en/product/stockwell-iii" target="_blank" rel="nofollow">$249 / £199</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>IP55</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3 with SBC, AAC and LC3, 3.5mm and USB-C wired</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>1x 65W Class D amplifier (3-inch woofer), 2x 31W Class D amplifiers (two 1.75-inch wide-band drivers)</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>54Hz-20KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>10m / 32.8ft</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>40 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>7.1 x 6.4 x 2.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.9lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black and Brass, Cream</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-price-availability"><span>Marshall Stockwell III review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aEjTrwiHyg39dANWmMDziJ" name="Marshall_Stockwell_III_Speaker_010.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/aEjTrwiHyg39dANWmMDziJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III was first announced on 9 June, 2026, over seven years after its predecessor, the Marshall Stockwell II. Available to buy from 18 August, the Stockwell III has a retail price of <a href="https://www.marshall.com/gb/en/product/stockwell-iii" target="_blank" rel="nofollow">$249 / £199</a> — same as the second-gen model at launch in 2019. The Stockwell III comes in Black or Cream, and both have brass accents.</p><p>This is a premium portable Bluetooth speaker, and it finds itself competing with the likes of the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ultimate-ears-everboom-review">Ultimate Ears Everboom</a> ($249) and the <a href="https://www.tomsguide.com/audio/smart-speakers/sonos-play-review">Sonos Play</a> ($299). Though a little expensive, it’s still cheaper than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/i-just-tested-this-portable-bluetooth-speaker-and-it-could-last-you-forever">Bang & Olufsen Beosound A1 3rd Gen</a> which costs a whopping $399. The Stockwell III is worth the investment if you want 40+ hours of battery and beautiful sound stuffed into a compact (and stunning) package.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-design"><span>Marshall Stockwell III review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PRJYvqsoqRKGpUWrmCbzaK" name="Marshall_Stockwell_III_Speaker_005.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/PRJYvqsoqRKGpUWrmCbzaK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III is, in a nutshell, a ridiculously sexy speaker, and <em>the </em>portable speaker to get if you care about aesthetics above all else. Not much has changed between the Stockwell III and its predecessor when it comes to portability. The third-gen model measures 7.1 x 6.4 x 2.9 inches and weighs 2.9lbs. To make it easier to carry around, Marshall has included a guitar-esque, velvet carry strap in the box, which can be attached to the Stockwell III’s sides.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XNVzTW39bgryrU2HX2EoZK" name="Marshall_Stockwell_III_Speaker_012.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/XNVzTW39bgryrU2HX2EoZK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Given its hefty weight, the Stockwell III isn’t the most portable speaker around. The Ultimate Ears Everboom is lighter at 2.1lbs, as is the Bang & Olufsen Beosound A1 3rd Gen which weighs 1.27lbs. The Stockwell III’s strap makes it slightly easier to carry around, but if you want an even lighter speaker but with Marshall’s looks, the <a href="https://www.tomsguide.com/audio/marshal-emberton-iii-review">Emberton III</a> ($149, 1.5lbs) is a standout choice.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3009px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="99LhosA5GaBQhamtjfA9oC" name="Marshall_Stockwell_III-2" alt="Marshall Stockwell III Bluetooth portable speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/99LhosA5GaBQhamtjfA9oC.jpg" mos="" align="middle" fullscreen="" width="3009" height="1693" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Stockwell III looks more premium compared to its predecessor, too. While the Stockwell II featured white accents, the Stockwell III now features brass branding (on either black or cream), keeping in line with the brand’s other current-gen speakers — and headphones, too, like the <a href="https://www.tomsguide.com/audio/headphones/marshall-milton-anc-review">Milton ANC</a> ($229) and the <a href="https://www.tomsguide.com/audio/headphones/marshall-monitor-iii-review">Monitor III </a>($349). I much prefer this look as the black/cream and gold combo makes the speaker stand out in a crowded market.</p><p>Build quality is solid, thanks to metal mesh grilles and the textured silicone outer sleeve. You’re getting bang for your buck with the Stockwell III.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LfwoDyV3HgiQB9RGxD3vYK" name="Marshall_Stockwell_III_Speaker_016.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/LfwoDyV3HgiQB9RGxD3vYK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>But why should you buy the Stockwell III instead of Marshall’s other more portable (ergo, lightweight) speakers? The Stockwell III is a mid-sized, premium Bluetooth speaker that’s clearly designed for both home (given the Placement Compensation feature within the app) and light outdoor use (given its IP55 rating).</p><p>It’s got a more luxurious feel to it than the ultra-portable Emberton III, for instance, but it’s smaller than a massive party speaker, like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Bromley 450</a> ($799). If you want a small speaker to travel with, and one that can withstand the elements, get the IP67-rated Emberton III. But if you want a mid-sized speaker that isn’t as expensive or bulky as the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review">Kilburn III </a>($379), get the Stockwell III.</p><h2 id="weatherproofing">Weatherproofing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kdmxA8EAKBuqKA2pnxB6nC" name="Marshall_Stockwell_III-3" alt="Marshall Stockwell III Bluetooth portable speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/kdmxA8EAKBuqKA2pnxB6nC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Marshall has made significant improvements to the Stockwell III’s durability as the third-gen model is now IP55-rated — the Stockwell II was only IPX4-rated. IP55 means the Stockwell III is protected from dust and is fairly water resistant, though you can’t submerge it underwater like you can the IP67-rated Ultimate Ears Everboom.</p><p>IP55 should suffice for most scenarios, but be mindful of using it in wet conditions repeatedly as any gadget’s waterproofing wears down over time.</p><h2 id="controls-3">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yYPayj7Q9yfiEWByt7xCaK" name="Marshall_Stockwell_III_Speaker_014.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/yYPayj7Q9yfiEWByt7xCaK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>All the buttons and dials are located on the Stockwell III’s top plate, and you’ll be familiar with them if you’ve used any of Marshall’s other speakers, like the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review">Acton IV</a> ($299). Even if you aren’t, getting to grips with the controls is quick and easy. All the buttons and dials give great tactile feedback when pressed or turned. </p><p>There’s a Source button for swapping between Bluetooth and AUX, and a customizable ‘M’ button that, by default, activates Spotify Tap so you can start playing music without opening the app on your phone; playback joystick; and three amp-like knobs for adjusting volume, treble and bass. Alongside these, Marshall has also added a handy battery indicator.</p><h2 id="connectivity-2">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JmLp57pYSrVCXwTN2mbKaK" name="Marshall_Stockwell_III_Speaker_008.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/JmLp57pYSrVCXwTN2mbKaK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Stockwell III features a 3.5mm jack and a USB-C port for wired playback, as well as Bluetooth 5.3 for wireless connectivity. Over Bluetooth, you get support for SBC, AAC and LC3 codecs — the latter delivers high efficiency and better sound quality at low bitrates. This also gives it a leg-up over the Ultimate Ears Everboom which supports SBC and AAC only.</p><p>While the Stockwell II featured traditional multi-speaker pairing with other Stockwell IIs, the Stockwell III is Auracast-enabled — and that’s better as it enables multiple devices to connect to a single audio source without pairing. The third-gen model utilizes Bluetooth LE to essentially connect to an unlimited number of other Auracast-enabled Marshall speakers, such as the Emberton III or other Stockwell IIIs.</p><p>You also get multipoint connectivity, which means that you can connect two different Bluetooth devices to the Stockwell III simultaneously. I did this with my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> and <a href="https://www.tomsguide.com/phones/google-pixel-phones/google-pixel-10-pro-xl-review">Google Pixel 10 Pro XL</a> and it worked seamlessly.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-sound-quality"><span>Marshall Stockwell III review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="cJuZ93UVeGvdiFfv48yRVJ" name="Marshall_Stockwell_III_Speaker_011.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/cJuZ93UVeGvdiFfv48yRVJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III is very loud for its size — it’s one of the best-sounding portable Bluetooth speakers around. Under the hood, the Stockwell III is fitted with a 65W Class D amplifier to power its 3-inch woofer, and two 31W Class D amplifiers for its dual 1.75-inch wide-band drivers (versus two 5W tweeters and a single 10W woofer on the Stockwell II). The speaker covers a frequency response range of 54Hz to 20KHz.</p><p>Marshall has clearly spared no expense when it comes to stuffing the Stockwell III to the brim — and the result is simply outstanding sound. To test the speaker, I listened to a range of songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for audiophiles. ‘<a href="https://open.qobuz.com/track/398890244" target="_blank" rel="nofollow">2515</a>’ by Wasia Project, which has a strong, pulsating bassline acting as the track’s backbone throughout, sounded sublime. The song’s dark electronic pulse came through with real weight, while the bass remained tight and controlled, and never became muddy — even at high volumes.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4ygwMmfKZdmg5ZcjJJTdZK" name="Marshall_Stockwell_III_Speaker_006.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/4ygwMmfKZdmg5ZcjJJTdZK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This is where Marshall’s ‘Dynamic Loudness’ comes into play. The Stockwell III borrows this from the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Stanmore IV</a> ($399) and the Acton IV, and it basically ensures that sound remains balanced and distortion-free regardless of the volume. Playing ‘2515’ at 80% volume didn’t overwhelm the speaker, as the lows maintained their warmth and richness.</p><p>I then listened to ‘<a href="https://open.qobuz.com/track/64405603" target="_blank" rel="nofollow">Mariners Apartment Complex</a>’ by Lana Del Rey to test the soundstage — and sure enough, the Stockwell III didn’t falter. Lana’s vocals sat front and center, with her words remaining clear and never bleeding into one another. There was a great balance between her remarkable vocals and the acoustic guitar and piano, and the strings sounded textured and defined. Also, the True Stereophonic 360° — found on the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Bromley 750</a> ($1,299) and Bromley 450 party speakers — filled the room and the song sounded consistent when I moved around, giving the track an effortless sense of space and depth.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eBSeskbUXDHVeK2sMS28XK" name="Marshall_Stockwell_III_Speaker_007.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/eBSeskbUXDHVeK2sMS28XK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Lastly, I played ‘<a href="https://open.qobuz.com/track/76767860" target="_blank" rel="nofollow">Papua New Guinea</a>’ by The Future Sound of London, an electronic, ambient techno track. The driving basslines, swirling synths, breakbeat-style percussion and oscillating keyboard loops all came together to create a layered production, and the Stockwell III reproduced the dreamy nature extremely well. The treble felt vibrant and nuanced without becoming fatiguing. My partner and I couldn’t help but move along to the song, like we were at a rave.</p><p><em>Listen along to my review with the Marshall Stockwell III testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/68631732?zone=GB-en"></iframe><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-app"><span>Marshall Stockwell III review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="e2sBa5XjBvQ7LiP4hrBGMK" name="Marshall_Stockwell_III_Speaker_013.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/e2sBa5XjBvQ7LiP4hrBGMK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Stockwell III is accompanied by the <a href="https://www.marshall.com/gb/en/support/speakers/learn/app-marshall-bluetooth" target="_blank" rel="nofollow">Marshall Bluetooth app</a>, available for free on iOS and Android. It’s the same app used by the British brand’s headphones and earbuds, and it’s your one-stop shop for adjusting the EQ or choosing a preset, updating the firmware, enabling Auracast, and customizing the ‘M’ button. The app sports a clean interface and is extremely easy to use.</p><p>You also get Placement Compensation, where you can select whether the speaker is close to an edge or corner, or flush against a wall. Once you’ve chosen your desired settings, the Stockwell III then corrects itself for any nearby reflective surfaces that may affect the sound quality. This feature has been borrowed from the Stanmore IV and Acton IV, which are plug-in home speakers. It’s handy to have if you’re keeping the Stanmore III in one place but given that it’s a portable Bluetooth speaker, I highly doubt you’ll be keeping it stationary.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/3FaDrwhoRqRafhHtBRDbtD.jpg" alt="Marshall Bluetooth app screenshots" /><figcaption><small role="credit">Marshall / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/MTA84CGuSfcTvezwcHYxxD.jpg" alt="Marshall Bluetooth app screenshots" /><figcaption><small role="credit">Marshall / Tom's Guide</small></figcaption></figure></figure><p>There’s a Source Mix option, too, which is new. It basically enables you to play music over Bluetooth <em>and </em>via USB-C at the same time. I can’t really think of a use case for this, but I suppose if you want to feel like a DJ, you can.</p><p>One thing I find surprising, though, is that the Stockwell III uses the Marshall Bluetooth app and not the new <a href="https://play.google.com/store/apps/details?id=com.marshallgroup.marshall&hl=en_GB" target="_blank" rel="nofollow">Marshall</a> app (came out in 2026 before the Stockwell III) which its fourth-gen home speakers utilize. Wouldn’t it make sense to have all of its speakers in one app? I’m personally not a fan of having two separate apps for different devices. Maybe it’s because the Stockwell III was actually created before the Stanmore IV and Acton IV were. Perhaps this is something Marshall will fix later. This isn’t a complaint, anyway, just an observation.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-battery-life"><span>Marshall Stockwell III review: Battery life</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:960px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aEjTrwiHyg39dANWmMDziJ" name="Marshall_Stockwell_III_Speaker_010.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/v2/t:108,l:439,cw:960,ch:540,q:80/aEjTrwiHyg39dANWmMDziJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Marshall has fitted a bigger battery into the Stockwell III, and the speaker can now run for over 40 hours on a single charge. That’s double the Stockwell II’s battery life. The Stockwell III trumps its competition too, as the Sonos Play is rated for 24 hours, and the Ultimate Ears Everboom is rated for 20. Even the pricier Bang & Olufsen Beosound A1 3rd Gen is rated for just 24 hours.</p><p>In my testing, I played at 50% volume over multiple listening sessions. The total time it took for the Stockwell III to run out of battery was just over 40 hours, keeping in line with Marshall’s claims.</p><p>Similar to the Sonos Play, the Stockwell III doubles as a power bank and you can use a USB-C cable to charge your smartphone, laptop and other devices. This is a great feature to have in a portable speaker as you don’t need to worry about finding a socket to charge your phone if you’re, say, out on a hike.</p><h2 class="article-body__section" id="section-marshall-stockwell-iii-review-verdict"><span>Marshall Stockwell III review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XCL39H5yGe7zmJRd3sMxXK" name="Marshall_Stockwell_III_Speaker_002.JPG" alt="Marshall Stockwell III portable Bluetooth speaker in black and brass" src="https://cdn.mos.cms.futurecdn.net/XCL39H5yGe7zmJRd3sMxXK.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Just when I thought Marshall speakers couldn’t get any better, the iconic amp manufacturer comes out with something like the Stockwell III. This is an excellent portable speaker that delivers far more sound than its compact design suggests. Though it isn’t cheap — just shy of $250 — its combination of rich bass, vibrant treble and clear mids make a strong case for the price.</p><p>The Stockwell III also packs 40 hours of battery life, IP55 durability, and immersive 360° sound into a slim profile. While it’s still not the lightest portable speaker around, the included velvet strap makes it easier to carry around.</p><p>I can’t help but describe the speaker’s sound as genuinely astounding. If I had to take one speaker with me on my travels or to picnics outdoors, it would be the Stockwell III. At the time of writing, it’s Marshall’s best portable Bluetooth speaker right now.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tested a music-lover's $1,499 all-in-one soundbar — and it's not by Samsung, Bose, or Sony ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Bluesound Pulse Cinema is an incredibly interesting soundbar. It’s $1,499, which puts it firmly in the “premium” category, and it’s all-in-one, which means it has two subwoofer channels in the soundbar itself. So, is all of this enough to make it one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> available in 2026? </p><p>Unfortunately, not quite. While the overall sound performance is great, a few dialogue issues (and no dialogue mode) prevented it from achieving full marks. Even so, I didn’t have to strain to understand 99% of dialogue — but those rare instances marred my experience. It’s pretty pricey for just a soundbar, and there’s not as much height as I would expect for $1,500. </p><p>It’s still a worthy contender, though, especially if you want a soundbar as part of a wider multi-speaker sound system. Want to find out more? Keep reading this Bluesound Pulse Cinema review. </p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-specs"><span>Bluesound Pulse Cinema review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Bluesound-Multi-Room-Streaming-Soundbar-Subwoofer/dp/B0FRV83WY2" target="_blank" rel="nofollow">$1,499</a> / <a href="https://www.richersounds.com/bluesound-pulse-cinema/" target="_blank" rel="nofollow">£1,249</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI, optical, AirPlay, Bluetooth, RCA, USB-A</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>14.8 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>47.1 x 2.9 x 5.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Gray</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>No, but sub out</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>3.2.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-price-availability"><span>Bluesound Pulse Cinema review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="miPq6L2SUXg5PtcXDWo6Yf" name="Bluesound_PulseCinema_soundbar 3.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/miPq6L2SUXg5PtcXDWo6Yf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bluesound Pulse Cinema is a pretty pricey soundbar, at <a href="https://www.amazon.com/Bluesound-Multi-Room-Streaming-Soundbar-Subwoofer/dp/B0FRV83WY2" target="_blank" rel="nofollow">$1,499 from Amazon U.S.</a> and <a href="https://www.richersounds.com/bluesound-pulse-cinema/" target="_blank" rel="nofollow">£1,249 from Richer Sounds in the U.K.</a></p><p>For a 3.2.2-channel soundbar, this is quite expensive. I’ve tested the 23-driver, 7.1.4-channel <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300xmk2-review">JBL Bar 1300X MKII</a>, which is only $300 pricier, at $1,699. I’ve also reviewed the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs90h-review">Samsung QS90H</a> ($899), which is fantastic if you want excellent bass without needing a separate subwoofer. </p><p>If money is no object, I’d recommend checking out the $2,999 <a href="https://www.tomsguide.com/audio/soundbars/yamaha-true-x-surround-90a-review">Yamaha True X Surround 90A</a>, which is the most immersive Dolby Atmos soundbar I’ve ever used. There’s also the fantastic <a href="https://www.tomsguide.com/audio/soundbars/kef-xio-review">KEF XIO</a>, $1,699, which has incredible sound quality and 360° audio. </p><p>At the cheaper end of the spectrum, I’d recommend the $399 <a href="https://www.tomsguide.com/audio/soundbars/ive-officially-found-my-new-favorite-mid-range-soundbar-it-made-me-feel-like-i-was-at-the-movies">Samsung Q600F</a>/H or the $699 <a href="https://www.tomsguide.com/audio/soundbars/denon-home-sound-bar-550-review">Denon Home 550</a> for soundbars with clarity and depth. </p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-design"><span>Bluesound Pulse Cinema review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="36QZoUEqMWhdH2ffbCiAqf" name="Bluesound_PulseCinema_soundbar 5.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/36QZoUEqMWhdH2ffbCiAqf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bluesound Pulse Cinema looks like any other soundbar. It’s pretty plain, but in a purposeful way that means it won’t steal attention away from the rest of your living room. Of course, there’s a mesh exterior and an on-bar button control pad. This illuminates red, green, and blue. </p><p>It’s quite long, at 47 inches, so I’d recommend it only for 55-inch TVs and up. Any smaller would look strange. If you’ve a smaller TV, you might be better suited to the 27-inch <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review">Bose Smart Soundbar </a>(2024), or the 26-inch Denon Home 550.</p><p>The soundbar was much lighter than I thought it would be: 14.8 pounds. Compared to the Yamaha True X Surround 90A’s 24-pound heft and the KEF XIO’s 23-pound bulk, this was noticeably easier to move around. You might not even need a friend to help you set it up. </p><h2 id="controls-4">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JXhBKBCwxd5U2ny6HLXyGf" name="Bluesound_PulseCinema_soundbar 8.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/JXhBKBCwxd5U2ny6HLXyGf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s no remote, so all your controlling is done via the BlueOS app. I found setting this up relatively straightforward. I had to restart it a couple of times, but it worked without any headache from me. I didn’t even have to set up my Wi-Fi network: the app took the information from AirPlay, which was great. No scrabbling around trying to find that pesky piece of paper taped to the back of my router. </p><p>Ideally, there would be a remote, too. What if your phone breaks and then you’re stuck without a soundbar remote? While this likely won’t be an issue 99% of the time, there’s always that 1%. </p><p>As I connected the soundbar via HDMI eARC, I was able to control it via my TV and Blu-ray player remotes, so I didn’t have any issues here. Specific adjustments like treble and bass volume controls were easy to make in the app. </p><h2 id="features-2">Features</h2><p>I was disappointed to see no dialogue mode or dialogue enhancer. I personally wouldn’t buy a soundbar without this, purely because I like the reassurance of being able to enhance my dialogue if required. </p><p>The Pulse Cinema does have “night”, “movie”, and “music” modes. There’s also a “surround upmixer”, “virtualizer”, and “volume leveler”, alongside treble and bass EQ adjustment. The surround upmixer does what it says on the tin — creates a virtual surround sound environment. “Virtualizer” does pretty much the same thing. I didn’t notice “volume leveler” making much of a change, but it’s supposed to make all the soundbar output the same volume. This would be good if you listen to poorly mastered media or if you’re struggling to hear dialogue.</p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-app"><span>Bluesound Pulse Cinema review: App</span></h2><p>The Bluesound Pulse Cinema uses the BlueOS app, which works almost identically to the Denon HEOS app. This is more of a multi-room smart speaker app than the soundbar-specific JBL One app. There’s no room calibration or individual channel adjustment, which is a shame for a $1,499 soundbar. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7WBQvMTcmbVJK3XgpeHoVL" name="bluesound pulse cinema screenshots" alt="three screenshots from the bluesound OS app showing connectivity functions with the pulse cinema soundbar" src="https://cdn.mos.cms.futurecdn.net/7WBQvMTcmbVJK3XgpeHoVL.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future / Bluesound)</span></figcaption></figure><p>As you can see, the app has TuneIn and integrates music streaming services like TIDAL and Qobuz, making it ideal for building a general multi-speaker home setup. It’s easy to use and easy to set up, so I have no criticisms here. I just wish it had some more soundbar/home cinema-specific modes.</p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-sound-quality"><span>Bluesound Pulse Cinema review: Sound quality</span></h2><p>I test every soundbar in the same room, with “Top Gun: Maverick” on 4K Blu-ray, various other Blu-rays, TV streaming, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-2">Movies</h2><p>As always, I started with “Top Gun: Maverick”. The opening scene is a goldmine for soundbar testing: high-frequency in-scene sound effects like whirring engines and whooshing ropes, bass-heavy “Danger Zone”, the roaring of jet engines flying overhead. </p><p>I could feel the planes flying around me, but I couldn’t hear them tracking over me. This is a similar issue on Samsung soundbars, namely the QS90H and the Q600F/H. I want height <em>and</em> width! I tested the Yamaha True Sound 90A in the same room, and it offered much more height, which is why it’s the best Dolby Atmos soundbar I’ve ever used — but it <em>is</em> $3,000. I’d also recommend the JBL Bar 1300X MKII as one of the best surround sound experiences — it had plenty of height in the same test, and is only $300 more than the Bluesound. </p><p>Whooshing ropes were clear, and the grinding of Tom Cruise’s wrench was audible in the following scene. The string-heavy soundtrack radiated through the scene like sunlight streaming through cloud cover — but I didn’t get as much width or depth as I wanted. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pGjZSaKJHq3BXkVU2Z7Mjf" name="Bluesound_PulseCinema_soundbar 7.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/pGjZSaKJHq3BXkVU2Z7Mjf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I also noticed some roughness around dialogue, as if the soundbar was struggling to differentiate the bass in Tom Cruise’s voice from the bass in the OST. This was also an issue in “How to Train Your Dragon (2025)”, but I’ll discuss that in detail below. </p><p>However, during action scenes, the bass performance was good. In the first flying scene — Maverick attempting Mach 10 — I could feel the cabin rattling around, the rumbling of the engine right before the explosion. I <em>did</em> want a little more during the explosion, though. </p><p>In the first dogfighting scene, the radioed dialogue was clear, despite the lack of dialogue enhancement mode. The rumbling bass of the jet engines didn’t overpower dialogue. I never struggled to make out speech in this scene. </p><p>I also watched “How to Train Your Dragon (2025)”. Hiccup’s voice-over and the OST were both clear in the opening action scene, but the dragon sound effects were the true standouts here, especially in the grove scene halfway through the movie. When Toothless flew away, the sound swarmed from around me.  </p><p>However, there were unpleasant crackling sounds when the characters were talking and moving. While this might be a “How to Train Your Dragon (2025)” mixing issue, I also experienced some crackling during Tom Cruise’s dialogue in “Top Gun: Maverick”. Amazon reviews mention “bass popping”, but I didn’t experience anything like this.</p><p>I also watched the original Disney “Alice in Wonderland”. This movie is encoded with Dolby Digital rather than true Atmos. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="y7Lp5gU9dysobcDZJsGd4f" name="Bluesound_PulseCinema_soundbar 9.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/y7Lp5gU9dysobcDZJsGd4f.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>“Alice in Wonderland” sounded fantastic. The soundbar had no issues switching from music to dialogue to ‘action’ scenes (Alice falling down the rabbit hole). When Alice fell, my stomach actually dropped thanks to the sound effects. The flutes sounded particularly excellent, and I never struggled to hear dialogue — but this isn’t exactly a blockbuster action movie. </p><p>As a final blockbuster test, I watched “The Batman (2022)”. The eerie, tense, string-heavy OST didn’t overpower the dialogue or in-scene TV sounds whilst screeching in the background of the scene. The falling rain was exceptionally wide; I could hear it as if it were actually pouring in the testing room. </p><p>The iconic “Something in the Way” scene was my favorite scene of the movie. I could hear Pattinson’s brooding voiceover clearly, despite the music and motorcycle sound effects. I could also make out the newsreader speech and the rewinding sound effects over the music. This performance was really great — much better than “How to Train Your Dragon (2025)”.</p><h2 id="tv-2">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WS2qA9Re9XxDXAvVTovBjf" name="Bluesound_PulseCinema_soundbar 6.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/WS2qA9Re9XxDXAvVTovBjf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV playback, I watched “Interview with the Vampire” on basic Netflix. I didn’t notice any crackling or dialogue hissing here. The saloon-inspired soundtrack danced across the soundstage, but never overpowered the dialogue. In-scene sound effects like crickets and crackling fires were clear and pronounced, but layered perfectly beneath dialogue. </p><p>I noticed some compression issues when the scenes switched between voiceover and retrospective scenes. “Interview with the Vampire” is that — an interview, taking place in modern day. The protagonist’s dialogue weaves from “present day” interview answers into the retrospective scenes of which he speaks. During each scene, I didn’t notice any processing issues, but during transitional scenes, some dialogue was compressed. </p><p>Overall, though, I enjoyed this experience. I never struggled to hear dialogue or make out high-frequency sound effects.</p><h2 id="music-2">Music</h2><p>To test music playback, I listened to a range of genres via Qobuz, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service for audiophiles</a>. ‘Von dutch (A.G. Cook remix ft. Addison Rae)’ by Charli xcx is an electronic track with processed synths and rolling bass. Right at the end of the track, there’s a spoken section. This section sounded <em>fantastic</em>. I felt like the words were bouncing around the room. </p><p>After, I played ‘I Was Neon’, an indie track with rounded female vocals and slightly crispy guitars. The percussion was a little muted, but not overly so, and the licks of electric guitar after the chorus were particularly melodic. While I wanted a little more control and complexity to the bass performance, at the end of the day, this is a soundbar, not a speaker. This music performance is impressive. </p><p>Lastly, I switched to heavier rock with ‘My God Is The Sun’ by Queens of the Stone Age. As with the Julia Jacklin track, the overall performance was good for a soundbar. Homme’s vocals were clear and propulsive, and the percussion wasn’t tinny. However, I wanted <em>more</em>. More bass, more complexity, more passion. At the end of the day, though, this is a soundbar. And soundbars will never be as impressive as genuine music-focused speakers.</p><p>Overall, for music, this is one of the best soundbars I’ve ever used. It matches the Bose Smart Soundbar (2024) and exceeds the Samsung Q600H and QS90H. </p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-verdict"><span>Bluesound Pulse Cinema review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="G9MedBLyu7tp94V44j3HNf" name="Bluesound_PulseCinema_soundbar 10.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/G9MedBLyu7tp94V44j3HNf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bluesound Pulse Cinema is a great soundbar, with impressive width and perhaps the best music on a soundbar I’ve ever experienced. The app is easy to use and straightforward, with no connectivity or setup issues. </p><p>Even so, for it to achieve our coveted 4-star rating, it would need a few changes: no dialogue/bass rumbling issues (as experienced during “Top Gun: Maverick” and “How to Train Your Dragon (2025)”, and more premium soundbar-specific features — or a lower price. When you can spend $300 extra for the near-perfect JBL Bar 1300X MK2, or save $600 and get the all-in-one Samsung QS90H, why wouldn’t you just do that? </p><p>I think I would buy the JBL Bar 1300X MK2 over this model due to the more expansive sound and room calibration tools, but the Bluesound Pulse Cinema is still a great option if you want to build a multi-room smart sound system. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/bluesound-pulse-cinema-review</link>
                                                                            <description>
                            <![CDATA[ The Bluesound Pulse Cinema is a pretty pricey all-in-one soundbar, but has great bass, great music performance, and great buildability. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">qvAyTscrMkYcWrqAhTnja</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/GnLFxHeLq6bkj2QRVrsmif-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 13 Aug 2026 12:40:55 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/GnLFxHeLq6bkj2QRVrsmif-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the bluesound pulse cinema soundbar photographed with a TV and the tom&#039;s guide blue background]]></media:description>                                                            <media:text><![CDATA[the bluesound pulse cinema soundbar photographed with a TV and the tom&#039;s guide blue background]]></media:text>
                                <media:title type="plain"><![CDATA[the bluesound pulse cinema soundbar photographed with a TV and the tom&#039;s guide blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/GnLFxHeLq6bkj2QRVrsmif-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Bluesound Pulse Cinema is an incredibly interesting soundbar. It’s $1,499, which puts it firmly in the “premium” category, and it’s all-in-one, which means it has two subwoofer channels in the soundbar itself. So, is all of this enough to make it one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> available in 2026? </p><p>Unfortunately, not quite. While the overall sound performance is great, a few dialogue issues (and no dialogue mode) prevented it from achieving full marks. Even so, I didn’t have to strain to understand 99% of dialogue — but those rare instances marred my experience. It’s pretty pricey for just a soundbar, and there’s not as much height as I would expect for $1,500. </p><p>It’s still a worthy contender, though, especially if you want a soundbar as part of a wider multi-speaker sound system. Want to find out more? Keep reading this Bluesound Pulse Cinema review. </p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-specs"><span>Bluesound Pulse Cinema review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Bluesound-Multi-Room-Streaming-Soundbar-Subwoofer/dp/B0FRV83WY2" target="_blank" rel="nofollow">$1,499</a> / <a href="https://www.richersounds.com/bluesound-pulse-cinema/" target="_blank" rel="nofollow">£1,249</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI, optical, AirPlay, Bluetooth, RCA, USB-A</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>14.8 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>47.1 x 2.9 x 5.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Gray</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>No, but sub out</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>3.2.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-price-availability"><span>Bluesound Pulse Cinema review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="miPq6L2SUXg5PtcXDWo6Yf" name="Bluesound_PulseCinema_soundbar 3.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/miPq6L2SUXg5PtcXDWo6Yf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bluesound Pulse Cinema is a pretty pricey soundbar, at <a href="https://www.amazon.com/Bluesound-Multi-Room-Streaming-Soundbar-Subwoofer/dp/B0FRV83WY2" target="_blank" rel="nofollow">$1,499 from Amazon U.S.</a> and <a href="https://www.richersounds.com/bluesound-pulse-cinema/" target="_blank" rel="nofollow">£1,249 from Richer Sounds in the U.K.</a></p><p>For a 3.2.2-channel soundbar, this is quite expensive. I’ve tested the 23-driver, 7.1.4-channel <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300xmk2-review">JBL Bar 1300X MKII</a>, which is only $300 pricier, at $1,699. I’ve also reviewed the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs90h-review">Samsung QS90H</a> ($899), which is fantastic if you want excellent bass without needing a separate subwoofer. </p><p>If money is no object, I’d recommend checking out the $2,999 <a href="https://www.tomsguide.com/audio/soundbars/yamaha-true-x-surround-90a-review">Yamaha True X Surround 90A</a>, which is the most immersive Dolby Atmos soundbar I’ve ever used. There’s also the fantastic <a href="https://www.tomsguide.com/audio/soundbars/kef-xio-review">KEF XIO</a>, $1,699, which has incredible sound quality and 360° audio. </p><p>At the cheaper end of the spectrum, I’d recommend the $399 <a href="https://www.tomsguide.com/audio/soundbars/ive-officially-found-my-new-favorite-mid-range-soundbar-it-made-me-feel-like-i-was-at-the-movies">Samsung Q600F</a>/H or the $699 <a href="https://www.tomsguide.com/audio/soundbars/denon-home-sound-bar-550-review">Denon Home 550</a> for soundbars with clarity and depth. </p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-design"><span>Bluesound Pulse Cinema review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="36QZoUEqMWhdH2ffbCiAqf" name="Bluesound_PulseCinema_soundbar 5.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/36QZoUEqMWhdH2ffbCiAqf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bluesound Pulse Cinema looks like any other soundbar. It’s pretty plain, but in a purposeful way that means it won’t steal attention away from the rest of your living room. Of course, there’s a mesh exterior and an on-bar button control pad. This illuminates red, green, and blue. </p><p>It’s quite long, at 47 inches, so I’d recommend it only for 55-inch TVs and up. Any smaller would look strange. If you’ve a smaller TV, you might be better suited to the 27-inch <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review">Bose Smart Soundbar </a>(2024), or the 26-inch Denon Home 550.</p><p>The soundbar was much lighter than I thought it would be: 14.8 pounds. Compared to the Yamaha True X Surround 90A’s 24-pound heft and the KEF XIO’s 23-pound bulk, this was noticeably easier to move around. You might not even need a friend to help you set it up. </p><h2 id="controls-4">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JXhBKBCwxd5U2ny6HLXyGf" name="Bluesound_PulseCinema_soundbar 8.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/JXhBKBCwxd5U2ny6HLXyGf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s no remote, so all your controlling is done via the BlueOS app. I found setting this up relatively straightforward. I had to restart it a couple of times, but it worked without any headache from me. I didn’t even have to set up my Wi-Fi network: the app took the information from AirPlay, which was great. No scrabbling around trying to find that pesky piece of paper taped to the back of my router. </p><p>Ideally, there would be a remote, too. What if your phone breaks and then you’re stuck without a soundbar remote? While this likely won’t be an issue 99% of the time, there’s always that 1%. </p><p>As I connected the soundbar via HDMI eARC, I was able to control it via my TV and Blu-ray player remotes, so I didn’t have any issues here. Specific adjustments like treble and bass volume controls were easy to make in the app. </p><h2 id="features-2">Features</h2><p>I was disappointed to see no dialogue mode or dialogue enhancer. I personally wouldn’t buy a soundbar without this, purely because I like the reassurance of being able to enhance my dialogue if required. </p><p>The Pulse Cinema does have “night”, “movie”, and “music” modes. There’s also a “surround upmixer”, “virtualizer”, and “volume leveler”, alongside treble and bass EQ adjustment. The surround upmixer does what it says on the tin — creates a virtual surround sound environment. “Virtualizer” does pretty much the same thing. I didn’t notice “volume leveler” making much of a change, but it’s supposed to make all the soundbar output the same volume. This would be good if you listen to poorly mastered media or if you’re struggling to hear dialogue.</p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-app"><span>Bluesound Pulse Cinema review: App</span></h2><p>The Bluesound Pulse Cinema uses the BlueOS app, which works almost identically to the Denon HEOS app. This is more of a multi-room smart speaker app than the soundbar-specific JBL One app. There’s no room calibration or individual channel adjustment, which is a shame for a $1,499 soundbar. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7WBQvMTcmbVJK3XgpeHoVL" name="bluesound pulse cinema screenshots" alt="three screenshots from the bluesound OS app showing connectivity functions with the pulse cinema soundbar" src="https://cdn.mos.cms.futurecdn.net/7WBQvMTcmbVJK3XgpeHoVL.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future / Bluesound)</span></figcaption></figure><p>As you can see, the app has TuneIn and integrates music streaming services like TIDAL and Qobuz, making it ideal for building a general multi-speaker home setup. It’s easy to use and easy to set up, so I have no criticisms here. I just wish it had some more soundbar/home cinema-specific modes.</p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-sound-quality"><span>Bluesound Pulse Cinema review: Sound quality</span></h2><p>I test every soundbar in the same room, with “Top Gun: Maverick” on 4K Blu-ray, various other Blu-rays, TV streaming, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-2">Movies</h2><p>As always, I started with “Top Gun: Maverick”. The opening scene is a goldmine for soundbar testing: high-frequency in-scene sound effects like whirring engines and whooshing ropes, bass-heavy “Danger Zone”, the roaring of jet engines flying overhead. </p><p>I could feel the planes flying around me, but I couldn’t hear them tracking over me. This is a similar issue on Samsung soundbars, namely the QS90H and the Q600F/H. I want height <em>and</em> width! I tested the Yamaha True Sound 90A in the same room, and it offered much more height, which is why it’s the best Dolby Atmos soundbar I’ve ever used — but it <em>is</em> $3,000. I’d also recommend the JBL Bar 1300X MKII as one of the best surround sound experiences — it had plenty of height in the same test, and is only $300 more than the Bluesound. </p><p>Whooshing ropes were clear, and the grinding of Tom Cruise’s wrench was audible in the following scene. The string-heavy soundtrack radiated through the scene like sunlight streaming through cloud cover — but I didn’t get as much width or depth as I wanted. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pGjZSaKJHq3BXkVU2Z7Mjf" name="Bluesound_PulseCinema_soundbar 7.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/pGjZSaKJHq3BXkVU2Z7Mjf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I also noticed some roughness around dialogue, as if the soundbar was struggling to differentiate the bass in Tom Cruise’s voice from the bass in the OST. This was also an issue in “How to Train Your Dragon (2025)”, but I’ll discuss that in detail below. </p><p>However, during action scenes, the bass performance was good. In the first flying scene — Maverick attempting Mach 10 — I could feel the cabin rattling around, the rumbling of the engine right before the explosion. I <em>did</em> want a little more during the explosion, though. </p><p>In the first dogfighting scene, the radioed dialogue was clear, despite the lack of dialogue enhancement mode. The rumbling bass of the jet engines didn’t overpower dialogue. I never struggled to make out speech in this scene. </p><p>I also watched “How to Train Your Dragon (2025)”. Hiccup’s voice-over and the OST were both clear in the opening action scene, but the dragon sound effects were the true standouts here, especially in the grove scene halfway through the movie. When Toothless flew away, the sound swarmed from around me.  </p><p>However, there were unpleasant crackling sounds when the characters were talking and moving. While this might be a “How to Train Your Dragon (2025)” mixing issue, I also experienced some crackling during Tom Cruise’s dialogue in “Top Gun: Maverick”. Amazon reviews mention “bass popping”, but I didn’t experience anything like this.</p><p>I also watched the original Disney “Alice in Wonderland”. This movie is encoded with Dolby Digital rather than true Atmos. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="y7Lp5gU9dysobcDZJsGd4f" name="Bluesound_PulseCinema_soundbar 9.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/y7Lp5gU9dysobcDZJsGd4f.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>“Alice in Wonderland” sounded fantastic. The soundbar had no issues switching from music to dialogue to ‘action’ scenes (Alice falling down the rabbit hole). When Alice fell, my stomach actually dropped thanks to the sound effects. The flutes sounded particularly excellent, and I never struggled to hear dialogue — but this isn’t exactly a blockbuster action movie. </p><p>As a final blockbuster test, I watched “The Batman (2022)”. The eerie, tense, string-heavy OST didn’t overpower the dialogue or in-scene TV sounds whilst screeching in the background of the scene. The falling rain was exceptionally wide; I could hear it as if it were actually pouring in the testing room. </p><p>The iconic “Something in the Way” scene was my favorite scene of the movie. I could hear Pattinson’s brooding voiceover clearly, despite the music and motorcycle sound effects. I could also make out the newsreader speech and the rewinding sound effects over the music. This performance was really great — much better than “How to Train Your Dragon (2025)”.</p><h2 id="tv-2">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WS2qA9Re9XxDXAvVTovBjf" name="Bluesound_PulseCinema_soundbar 6.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/WS2qA9Re9XxDXAvVTovBjf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV playback, I watched “Interview with the Vampire” on basic Netflix. I didn’t notice any crackling or dialogue hissing here. The saloon-inspired soundtrack danced across the soundstage, but never overpowered the dialogue. In-scene sound effects like crickets and crackling fires were clear and pronounced, but layered perfectly beneath dialogue. </p><p>I noticed some compression issues when the scenes switched between voiceover and retrospective scenes. “Interview with the Vampire” is that — an interview, taking place in modern day. The protagonist’s dialogue weaves from “present day” interview answers into the retrospective scenes of which he speaks. During each scene, I didn’t notice any processing issues, but during transitional scenes, some dialogue was compressed. </p><p>Overall, though, I enjoyed this experience. I never struggled to hear dialogue or make out high-frequency sound effects.</p><h2 id="music-2">Music</h2><p>To test music playback, I listened to a range of genres via Qobuz, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service for audiophiles</a>. ‘Von dutch (A.G. Cook remix ft. Addison Rae)’ by Charli xcx is an electronic track with processed synths and rolling bass. Right at the end of the track, there’s a spoken section. This section sounded <em>fantastic</em>. I felt like the words were bouncing around the room. </p><p>After, I played ‘I Was Neon’, an indie track with rounded female vocals and slightly crispy guitars. The percussion was a little muted, but not overly so, and the licks of electric guitar after the chorus were particularly melodic. While I wanted a little more control and complexity to the bass performance, at the end of the day, this is a soundbar, not a speaker. This music performance is impressive. </p><p>Lastly, I switched to heavier rock with ‘My God Is The Sun’ by Queens of the Stone Age. As with the Julia Jacklin track, the overall performance was good for a soundbar. Homme’s vocals were clear and propulsive, and the percussion wasn’t tinny. However, I wanted <em>more</em>. More bass, more complexity, more passion. At the end of the day, though, this is a soundbar. And soundbars will never be as impressive as genuine music-focused speakers.</p><p>Overall, for music, this is one of the best soundbars I’ve ever used. It matches the Bose Smart Soundbar (2024) and exceeds the Samsung Q600H and QS90H. </p><h2 class="article-body__section" id="section-bluesound-pulse-cinema-review-verdict"><span>Bluesound Pulse Cinema review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="G9MedBLyu7tp94V44j3HNf" name="Bluesound_PulseCinema_soundbar 10.JPG" alt="the bluesound pulse cinema soundbar photographed with a TV and the tom's guide blue background" src="https://cdn.mos.cms.futurecdn.net/G9MedBLyu7tp94V44j3HNf.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bluesound Pulse Cinema is a great soundbar, with impressive width and perhaps the best music on a soundbar I’ve ever experienced. The app is easy to use and straightforward, with no connectivity or setup issues. </p><p>Even so, for it to achieve our coveted 4-star rating, it would need a few changes: no dialogue/bass rumbling issues (as experienced during “Top Gun: Maverick” and “How to Train Your Dragon (2025)”, and more premium soundbar-specific features — or a lower price. When you can spend $300 extra for the near-perfect JBL Bar 1300X MK2, or save $600 and get the all-in-one Samsung QS90H, why wouldn’t you just do that? </p><p>I think I would buy the JBL Bar 1300X MK2 over this model due to the more expansive sound and room calibration tools, but the Bluesound Pulse Cinema is still a great option if you want to build a multi-room smart sound system. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Samsung Q600H review: Proof you don’t need to spend thousands on your home audio setup ]]></title>
                                                                                                <dc:content><![CDATA[ <p>If you want one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a> in your home, you could be spending thousands and thousands of your hard-earned cash. But what if I told you you could upgrade your TV’s sound for less than £350? I know — shocking. The Samsung Q600H has a good balance between voice, SFX, and OST. It’s a sleek, easy-to-use, budget-friendly soundbar and can be customized to suit you using the SmartThings app. </p><p>But be aware that, since it’s so cheap, the sound lacks the width and height you’d usually get from more expensive soundbars. This model is also only available in the U.K. and E.U. markets, but if you’re in the U.S., you can pick up last year’s model, the <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>, instead. We gave this soundbar 4.5 stars, and it’s our top choice mid-range option to spruce up your home sound system.</p><p>To find out more about the new soundbar, you can keep reading this full Samsung Q600H review.</p><h2 class="article-body__section" id="section-samsung-q600h-review-specs"><span>Samsung Q600H review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.co.uk/Samsung-3-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMPPZ6Y" target="_blank" rel="nofollow">£349</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, Wi-Fi, HDMI, Optical</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>29.1 lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>46.5 x 18.4 x 10.7 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colour</strong></p></td><td  ><p>Titan Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>3.1.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-samsung-q600h-review-price-availability"><span>Samsung Q600H review: Price & availability</span></h2><p>The Samsung Q600H retails at full price for <a href="https://www.samsung.com/uk/audio-devices/soundbar/q600h-black-hw-q600h-xu/">£549 on the Samsung website</a>, but you can find it cheaper on <a href="https://www.amazon.co.uk/Samsung-3-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMPPZ6Y">Amazon U.K. for £349</a> or at <a href="https://www.currys.co.uk/products/samsung-hwq600hxu-3.1.2-wireless-sound-bar-with-dolby-atmos-10304081.html">Currys for £499</a>. This soundbar is only available in the U.K. and E.U., which is a shame for those of you who are stateside, as this is a really great soundbar for the price. </p><p>If you’re in the U.S., the closest soundbar would be the <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>. The F is the 2025 iteration of the Q600H and costs just <a href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8">$379 at Amazon U.S.</a> If you’re in the U.K. and are happy to buy a model from the previous year, the <a href="https://www.amazon.co.uk/Soundbar-Subwoofer-SpaceFit-Q-Symphony-Adaptive/dp/B0DY1Z31MC">Q600F will cost you £221 at Amazon U.K.</a> We gave this soundbar 4.5 stars when we reviewed it last year, and it remains one of our top picks, so I’d recommend it if you’re trying to save money without sacrificing performance.</p><p>For a soundbar with more room-filling sound and better width and height distinction, check out the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> for $899/£699. This all-in-one soundbar performs well beyond its price tag and is suited for anyone who wants a more expansive sound or needs separate rear speakers.</p><p>And if you don’t want to spend too much money because this is your first venture into the world of soundbars, the <a href="https://www.tomsguide.com/audio/soundbars/klipsch-flexus-core-100-review">Klipsch Flexus Core 100</a> could be a great option. It’s very affordable at $349/£299 and gives you the option to add on a subwoofer, but lacks Wi-Fi connectivity, which I found very beneficial from the Q600H for AirPlay and Google Cast.</p><h2 class="article-body__section" id="section-samsung-q600h-review-design"><span>Samsung Q600H review: Design</span></h2><p>The Samsung Q600H doesn’t exactly stand out from the crowd. It looks like every other soundbar I’ve seen. But that’s a good thing. I appreciated that when I was watching TV, it was barely noticeable when set up underneath. The Titan Black Colour looks great and isn’t distracting. </p><p>The unit weighs 29.1 pounds, and I found it easy to lift and set up by myself. It measures 46.5 x 18.4 x 10.7 inches, so it doesn’t take up too much room either. I set the subwoofer up off to the side of my TV stand, and it looked good, without drawing too much attention.</p><h2 id="controls-5">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jyySpNneP4DXjdSuk3LDCN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/jyySpNneP4DXjdSuk3LDCN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>You can adjust the sound using the on-board buttons at the top of the soundbar, or you can use the included remote control. I get a little overwhelmed when I have multiple remote controls on the go. One for the TV, Blu-ray player and soundbar can all get a bit much. Luckily, the remote of the Q600H is very thin so doesn’t feel cumbersome — it feels more like a genuinely useful addition. I also controlled settings such as sound mode and EQ through the SmartThings app on my phone, which was straightforward, which I’ll get into in more detail later on.</p><h2 id="features-3">Features</h2><p>The Q600H soundbar has customisable sound modes, EQ adjustment for bass and treble, night mode, and voice enhancement. Most of this can be controlled via the SmartThings app, which I’ll discuss more below. </p><h2 class="article-body__section" id="section-samsung-q600h-review-app"><span>Samsung Q600H review: App</span></h2><p>This was my first time using the SmartThings app, and I was blown away by how easy it was. In my job, I have used a whole host of smart home apps, but this has to be one of the most straightforward to navigate. </p><p>In the app, you can switch between different sound modes. I found Standard mode the best for balanced audio and natural sound when watching sitcoms. Surround mode gave me more expansive sound for action sound effects. AI Adaptive sound was the best for detail, prioritizing ambient sounds over the soundtrack. The soundbar also has Game Pro mode, but unfortunately no dedicated music mode. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pyhSykRnrHAxsDkTeuMKpN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/pyhSykRnrHAxsDkTeuMKpN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>You can control EQ settings in the app, as well as the level on the subwoofer. I enjoyed the level of customization I got with the soundbar, but you get more freedom by paying a premium price for something like the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs90h-review">Samsung QS90H</a> ($999/£899). The QS90H is an all-in-one system with a 7.1.2 channel configuration — a lot more powerful than the 3.1.2 of the Q600H.</p><h2 class="article-body__section" id="section-samsung-q600h-review-sound-quality"><span>Samsung Q600H review: Sound quality </span></h2><p>I tested the Samsung Q600H soundbar with our standard watch of “Top Gun: Maverick”, as well as a few other films I know and love, TV streaming, and music streaming via AirPlay.</p><h2 id="movies-3">Movies</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tuBzLohaTjAqmhYESnFxoN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/tuBzLohaTjAqmhYESnFxoN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I watched “Top Gun: Maverick”, “La La Land”, and “The Fantastic Four: First Steps” on 4K Blu-ray. The opening scene of “Top Gun: Maverick” allowed me to evaluate the width, height, high-frequency sound and bass. I enabled AI Adaptive Sound and increased the bass. I also increased the subwoofer to the max to get the full effect of the rumble of the plane engines. In this scene, the high-frequency sounds such as ropes swishing and clanging chains were very clear — I could hear everything distinctly, but not as crisp as when using the Samsung QS90H. The bass of the jet engines stirring to life was well-balanced without rumbling, and the song “Danger Zone” wasn’t overpowered by the planes.</p><p>Using AI Adaptive Sound prioritizes diegetic sounds (things like sound effects, foley, vocals etc), as I was able to hear high-frequency details such as the light ticking of a clock, and the dialogue remains incredibly clear. In the scene where we see the Darkstar hypersonic plane take off for its Mach 10 flight, I didn’t get the impact I was anticipating with the sonic boom. While the radio dialogue in the dogfight scene was clear over the OST, it’s also worth noting that there wasn’t a great deal of width or height, and while this soundbar has SpaceFit Sound Pro, I was unable to test it as I didn’t have a compatible Samsung TV to hand — despite testing in a room with 10 TVs. </p><p>In the movie’s beach scene, the track “I Ain’t Worried” by OneRepublic playing over the scene demonstrated impeccable bass performance and great balance between dialogue and overture tracks. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="r9qoa9iBffi2dXoPC9NLmM" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/r9qoa9iBffi2dXoPC9NLmM.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Next, I watched “La La Land”, a film that I think has one of the best original soundtracks of all time (yes, they should have won that Oscar). After watching this, I would wholeheartedly recommend the soundbar for musical lovers, despite the lack of music mode. During “Another Day of Sun”, I could clearly hear the footsteps of the actors and dancers as they jumped across cars and slammed doors. Hearing these sounds so distinctly added a lot of detail to the scenes. </p><p>Throughout the movie, the dialogue was clear when the characters spoke at the same time as the OST. When the four roommates were getting ready, I could hear the echo in the bathroom set and, later, the clacking of their heels on the street.</p><p>Speaking of click-clacking, I was actually disappointed when listening to the “Lovely Night” dance scene. I could hardly hear Mia and Sebastian’s tap shoes, but other sounds such as the kicking up of dust were clear as day. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ph3p7Ky2JPSRwFhxoUZmhN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/ph3p7Ky2JPSRwFhxoUZmhN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Finally, I watched “The Fantastic Four: First Steps”. Sound effects from the movements of the Silver Surfer felt very expansive; however, other SFX sounds weren’t very clear. I switched to Surround mode, which heightened the  SFX and resulted in a more immersive experience. For example, when Herbie “speaks”. However, conversation from the human characters wasn’t as clear. But in general, I enjoyed watching this movie with the soundbar and think it elevated my viewing experience.</p><h2 id="tv-3">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tTThe7gWMRZZuMrbqzERtN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/tTThe7gWMRZZuMrbqzERtN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV performance, I watched ‘Invincible’ on Prime Video. I watched using the AI Adaptive Sound setting in the SmartThings app, and increased the bass and treble, with the subwoofer set to the highest setting. </p><p>The first episode of the TV show had a whole lot of action sound effects of a battle, with a lot of dialogue included. Indistinct chatter in the background of scenes, such as when Mark is walking through the school halls, was audible without taking over the soundstage, suggesting that the soundbar handled the layers of sound well.</p><p>The subwoofer kicked in for sound effects such as punching and more gruesome sounds like blood splatters, which were emphasised well, without taking over from the dialogue or soundtrack. I could also hear effects like the splatter of blood and guts moving across the room in accordance with the on-screen movements (gross!).</p><h2 id="music-3">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vDoyutUqdAXefN8q7kGqkN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/vDoyutUqdAXefN8q7kGqkN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the music playback performance, I listened to a couple of different songs using AirPlay. There’s no dedicated music listening mode on the soundbar. Firstly, I listened to “High and Low” by Empire of the Sun. This soundbar demonstrated excellent bass performance during synth sounds; however, the treble sounded harsh. I turned down the treble in the SmartThings app, which helped to improve the vocals, but it still wasn’t great. </p><p>Next, I listened to “Famous Last Words” by My Chemical Romance. This didn’t sound as forceful as I’m used to, and the layers of the song sounded disjointed. There was nowhere near enough depth to do the song justice. The stripped-back bridge resulted in a better performance because there were fewer components to confuse the soundbar, but when it built up again, it struggled to keep up. </p><p>Finally, I listened to an acoustic track, “Cariad” by The Royston Club. The stripped-back vocals sound amazing and actually made me exclaim “Oh! That’s good!” in shock. But then when the drums kicked in, they overpowered the acoustic guitar. Bass performance is seemingly the priority with this soundbar.</p><h2 class="article-body__section" id="section-samsung-q600h-review-verdict"><span>Samsung Q600H review: Verdict </span></h2><p>The Samsung Q600H is an ideal option for a cheap, entry-level soundbar for anyone who wants to enhance their at-home movie watching on a budget. I’ve found it incredibly easy to use with the included remote control or the SmartThings app, and the smart features make it feel highly customisable to fit your desired sound. I appreciated the great balance between dialogue, SFX, and OST sound, and the sleek, unobtrusive design is the cherry on top. </p><p>My only real qualm with the soundbar is the lack of width and height in the soundstage, but for less than £350, it’s something I’m willing to overlook. Unfortunately, this soundbar is only available to purchase in the U.K. or E.U., with no plan for a U.S. release. However, you can purchase last year's model, the Samsung Q600F, in the U.S.</p><p>Overall, the Samsung Q600H is a solid option to upgrade your home theatre, and one I’ll be making use of to watch the best new movie releases.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/samsung-q600h-review</link>
                                                                            <description>
                            <![CDATA[ The Samsung Q600H is a home audio bargain hunter’s dream with a sub-£350 price and great sound balance. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">oFvrVsKNLUNpiprsUDuHDC</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/bX8rVxMUzNeQnq5rFcqRkN-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 31 Jul 2026 15:49:34 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ ashley.thieme@futurenet.com (Ashley Thieme) ]]></author>                    <dc:creator><![CDATA[ Ashley Thieme ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/3AWovHjApwuNrSGRS6WBcL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Ashley Thieme is a staff writer on the Reviews team at Tom’s Guide where she tests out the latest tech so you can know what’s going to be worth your time and money. She has a master’s degree in Magazine Journalism and a bachelor’s degree in Journalism, Media and Sociology from Cardiff University. She has bylines in titles including Women’s Health UK, sharing the latest health and wellness news stories, and Virgin Radio UK, providing the latest entertainment news and working on celebrity interviews. She has experience reporting on a variety of topics including music, literature, motorsport, entertainment and health. In previously published work, she has reviewed live music events, books, and wellness products. She values the importance of tech enhancing your life rather than taking over, and as a music fanatic, she is always looking for the best way to listen to new music releases. Discovering the top audio equipment that enhances sound quality and provides optimum comfort is what Ashley does best. In her spare time, Ashley enjoys hitting her reading goals on Goodreads by getting into the latest novels with a cup of tea as well as getting out in the Welsh mountains for a good hike on the weekend.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/bX8rVxMUzNeQnq5rFcqRkN-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Samsung Q600H photographed in a livingroom ]]></media:description>                                                            <media:text><![CDATA[Samsung Q600H photographed in a livingroom ]]></media:text>
                                <media:title type="plain"><![CDATA[Samsung Q600H photographed in a livingroom ]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/bX8rVxMUzNeQnq5rFcqRkN-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>If you want one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a> in your home, you could be spending thousands and thousands of your hard-earned cash. But what if I told you you could upgrade your TV’s sound for less than £350? I know — shocking. The Samsung Q600H has a good balance between voice, SFX, and OST. It’s a sleek, easy-to-use, budget-friendly soundbar and can be customized to suit you using the SmartThings app. </p><p>But be aware that, since it’s so cheap, the sound lacks the width and height you’d usually get from more expensive soundbars. This model is also only available in the U.K. and E.U. markets, but if you’re in the U.S., you can pick up last year’s model, the <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>, instead. We gave this soundbar 4.5 stars, and it’s our top choice mid-range option to spruce up your home sound system.</p><p>To find out more about the new soundbar, you can keep reading this full Samsung Q600H review.</p><h2 class="article-body__section" id="section-samsung-q600h-review-specs"><span>Samsung Q600H review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.co.uk/Samsung-3-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMPPZ6Y" target="_blank" rel="nofollow">£349</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, Wi-Fi, HDMI, Optical</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>29.1 lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>46.5 x 18.4 x 10.7 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colour</strong></p></td><td  ><p>Titan Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>3.1.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-samsung-q600h-review-price-availability"><span>Samsung Q600H review: Price & availability</span></h2><p>The Samsung Q600H retails at full price for <a href="https://www.samsung.com/uk/audio-devices/soundbar/q600h-black-hw-q600h-xu/">£549 on the Samsung website</a>, but you can find it cheaper on <a href="https://www.amazon.co.uk/Samsung-3-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMPPZ6Y">Amazon U.K. for £349</a> or at <a href="https://www.currys.co.uk/products/samsung-hwq600hxu-3.1.2-wireless-sound-bar-with-dolby-atmos-10304081.html">Currys for £499</a>. This soundbar is only available in the U.K. and E.U., which is a shame for those of you who are stateside, as this is a really great soundbar for the price. </p><p>If you’re in the U.S., the closest soundbar would be the <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>. The F is the 2025 iteration of the Q600H and costs just <a href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8">$379 at Amazon U.S.</a> If you’re in the U.K. and are happy to buy a model from the previous year, the <a href="https://www.amazon.co.uk/Soundbar-Subwoofer-SpaceFit-Q-Symphony-Adaptive/dp/B0DY1Z31MC">Q600F will cost you £221 at Amazon U.K.</a> We gave this soundbar 4.5 stars when we reviewed it last year, and it remains one of our top picks, so I’d recommend it if you’re trying to save money without sacrificing performance.</p><p>For a soundbar with more room-filling sound and better width and height distinction, check out the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> for $899/£699. This all-in-one soundbar performs well beyond its price tag and is suited for anyone who wants a more expansive sound or needs separate rear speakers.</p><p>And if you don’t want to spend too much money because this is your first venture into the world of soundbars, the <a href="https://www.tomsguide.com/audio/soundbars/klipsch-flexus-core-100-review">Klipsch Flexus Core 100</a> could be a great option. It’s very affordable at $349/£299 and gives you the option to add on a subwoofer, but lacks Wi-Fi connectivity, which I found very beneficial from the Q600H for AirPlay and Google Cast.</p><h2 class="article-body__section" id="section-samsung-q600h-review-design"><span>Samsung Q600H review: Design</span></h2><p>The Samsung Q600H doesn’t exactly stand out from the crowd. It looks like every other soundbar I’ve seen. But that’s a good thing. I appreciated that when I was watching TV, it was barely noticeable when set up underneath. The Titan Black Colour looks great and isn’t distracting. </p><p>The unit weighs 29.1 pounds, and I found it easy to lift and set up by myself. It measures 46.5 x 18.4 x 10.7 inches, so it doesn’t take up too much room either. I set the subwoofer up off to the side of my TV stand, and it looked good, without drawing too much attention.</p><h2 id="controls-5">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jyySpNneP4DXjdSuk3LDCN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/jyySpNneP4DXjdSuk3LDCN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>You can adjust the sound using the on-board buttons at the top of the soundbar, or you can use the included remote control. I get a little overwhelmed when I have multiple remote controls on the go. One for the TV, Blu-ray player and soundbar can all get a bit much. Luckily, the remote of the Q600H is very thin so doesn’t feel cumbersome — it feels more like a genuinely useful addition. I also controlled settings such as sound mode and EQ through the SmartThings app on my phone, which was straightforward, which I’ll get into in more detail later on.</p><h2 id="features-3">Features</h2><p>The Q600H soundbar has customisable sound modes, EQ adjustment for bass and treble, night mode, and voice enhancement. Most of this can be controlled via the SmartThings app, which I’ll discuss more below. </p><h2 class="article-body__section" id="section-samsung-q600h-review-app"><span>Samsung Q600H review: App</span></h2><p>This was my first time using the SmartThings app, and I was blown away by how easy it was. In my job, I have used a whole host of smart home apps, but this has to be one of the most straightforward to navigate. </p><p>In the app, you can switch between different sound modes. I found Standard mode the best for balanced audio and natural sound when watching sitcoms. Surround mode gave me more expansive sound for action sound effects. AI Adaptive sound was the best for detail, prioritizing ambient sounds over the soundtrack. The soundbar also has Game Pro mode, but unfortunately no dedicated music mode. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pyhSykRnrHAxsDkTeuMKpN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/pyhSykRnrHAxsDkTeuMKpN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>You can control EQ settings in the app, as well as the level on the subwoofer. I enjoyed the level of customization I got with the soundbar, but you get more freedom by paying a premium price for something like the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs90h-review">Samsung QS90H</a> ($999/£899). The QS90H is an all-in-one system with a 7.1.2 channel configuration — a lot more powerful than the 3.1.2 of the Q600H.</p><h2 class="article-body__section" id="section-samsung-q600h-review-sound-quality"><span>Samsung Q600H review: Sound quality </span></h2><p>I tested the Samsung Q600H soundbar with our standard watch of “Top Gun: Maverick”, as well as a few other films I know and love, TV streaming, and music streaming via AirPlay.</p><h2 id="movies-3">Movies</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tuBzLohaTjAqmhYESnFxoN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/tuBzLohaTjAqmhYESnFxoN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I watched “Top Gun: Maverick”, “La La Land”, and “The Fantastic Four: First Steps” on 4K Blu-ray. The opening scene of “Top Gun: Maverick” allowed me to evaluate the width, height, high-frequency sound and bass. I enabled AI Adaptive Sound and increased the bass. I also increased the subwoofer to the max to get the full effect of the rumble of the plane engines. In this scene, the high-frequency sounds such as ropes swishing and clanging chains were very clear — I could hear everything distinctly, but not as crisp as when using the Samsung QS90H. The bass of the jet engines stirring to life was well-balanced without rumbling, and the song “Danger Zone” wasn’t overpowered by the planes.</p><p>Using AI Adaptive Sound prioritizes diegetic sounds (things like sound effects, foley, vocals etc), as I was able to hear high-frequency details such as the light ticking of a clock, and the dialogue remains incredibly clear. In the scene where we see the Darkstar hypersonic plane take off for its Mach 10 flight, I didn’t get the impact I was anticipating with the sonic boom. While the radio dialogue in the dogfight scene was clear over the OST, it’s also worth noting that there wasn’t a great deal of width or height, and while this soundbar has SpaceFit Sound Pro, I was unable to test it as I didn’t have a compatible Samsung TV to hand — despite testing in a room with 10 TVs. </p><p>In the movie’s beach scene, the track “I Ain’t Worried” by OneRepublic playing over the scene demonstrated impeccable bass performance and great balance between dialogue and overture tracks. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="r9qoa9iBffi2dXoPC9NLmM" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/r9qoa9iBffi2dXoPC9NLmM.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Next, I watched “La La Land”, a film that I think has one of the best original soundtracks of all time (yes, they should have won that Oscar). After watching this, I would wholeheartedly recommend the soundbar for musical lovers, despite the lack of music mode. During “Another Day of Sun”, I could clearly hear the footsteps of the actors and dancers as they jumped across cars and slammed doors. Hearing these sounds so distinctly added a lot of detail to the scenes. </p><p>Throughout the movie, the dialogue was clear when the characters spoke at the same time as the OST. When the four roommates were getting ready, I could hear the echo in the bathroom set and, later, the clacking of their heels on the street.</p><p>Speaking of click-clacking, I was actually disappointed when listening to the “Lovely Night” dance scene. I could hardly hear Mia and Sebastian’s tap shoes, but other sounds such as the kicking up of dust were clear as day. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ph3p7Ky2JPSRwFhxoUZmhN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/ph3p7Ky2JPSRwFhxoUZmhN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Finally, I watched “The Fantastic Four: First Steps”. Sound effects from the movements of the Silver Surfer felt very expansive; however, other SFX sounds weren’t very clear. I switched to Surround mode, which heightened the  SFX and resulted in a more immersive experience. For example, when Herbie “speaks”. However, conversation from the human characters wasn’t as clear. But in general, I enjoyed watching this movie with the soundbar and think it elevated my viewing experience.</p><h2 id="tv-3">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tTThe7gWMRZZuMrbqzERtN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/tTThe7gWMRZZuMrbqzERtN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV performance, I watched ‘Invincible’ on Prime Video. I watched using the AI Adaptive Sound setting in the SmartThings app, and increased the bass and treble, with the subwoofer set to the highest setting. </p><p>The first episode of the TV show had a whole lot of action sound effects of a battle, with a lot of dialogue included. Indistinct chatter in the background of scenes, such as when Mark is walking through the school halls, was audible without taking over the soundstage, suggesting that the soundbar handled the layers of sound well.</p><p>The subwoofer kicked in for sound effects such as punching and more gruesome sounds like blood splatters, which were emphasised well, without taking over from the dialogue or soundtrack. I could also hear effects like the splatter of blood and guts moving across the room in accordance with the on-screen movements (gross!).</p><h2 id="music-3">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vDoyutUqdAXefN8q7kGqkN" name="Samsung Q600H" alt="Samsung Q600H photographed in a livingroom" src="https://cdn.mos.cms.futurecdn.net/vDoyutUqdAXefN8q7kGqkN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the music playback performance, I listened to a couple of different songs using AirPlay. There’s no dedicated music listening mode on the soundbar. Firstly, I listened to “High and Low” by Empire of the Sun. This soundbar demonstrated excellent bass performance during synth sounds; however, the treble sounded harsh. I turned down the treble in the SmartThings app, which helped to improve the vocals, but it still wasn’t great. </p><p>Next, I listened to “Famous Last Words” by My Chemical Romance. This didn’t sound as forceful as I’m used to, and the layers of the song sounded disjointed. There was nowhere near enough depth to do the song justice. The stripped-back bridge resulted in a better performance because there were fewer components to confuse the soundbar, but when it built up again, it struggled to keep up. </p><p>Finally, I listened to an acoustic track, “Cariad” by The Royston Club. The stripped-back vocals sound amazing and actually made me exclaim “Oh! That’s good!” in shock. But then when the drums kicked in, they overpowered the acoustic guitar. Bass performance is seemingly the priority with this soundbar.</p><h2 class="article-body__section" id="section-samsung-q600h-review-verdict"><span>Samsung Q600H review: Verdict </span></h2><p>The Samsung Q600H is an ideal option for a cheap, entry-level soundbar for anyone who wants to enhance their at-home movie watching on a budget. I’ve found it incredibly easy to use with the included remote control or the SmartThings app, and the smart features make it feel highly customisable to fit your desired sound. I appreciated the great balance between dialogue, SFX, and OST sound, and the sleek, unobtrusive design is the cherry on top. </p><p>My only real qualm with the soundbar is the lack of width and height in the soundstage, but for less than £350, it’s something I’m willing to overlook. Unfortunately, this soundbar is only available to purchase in the U.K. or E.U., with no plan for a U.S. release. However, you can purchase last year's model, the Samsung Q600F, in the U.S.</p><p>Overall, the Samsung Q600H is a solid option to upgrade your home theatre, and one I’ll be making use of to watch the best new movie releases.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I thought my phone's speakers were good, but after listening to this $64 Bluetooth speaker I can never go back ]]></title>
                                                                                                <dc:content><![CDATA[ <p>This isn’t the first time I’ve written about the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit Stormbox Micro 3</a>, and it probably won’t be the last. It’s the best-sounding Bluetooth speaker I’ve tried, and it delivers amazingly big sound considering its small size. I didn’t think I was that big of a music fan, but this Bluetooth speaker has me totally converted.</p><p>Right now you can pick up the <a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">Tribit Stormbox Micro 3 for $64 at Amazon</a>. Every now and then it goes on sale for $49 or less, so keep an eye out for deals! </p><div class="product"><a data-dimension112="d0e7e9c4-8c00-11f1-a222-9ba87d24e3f3" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$64" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="LDHzrLzgPGmoS2J4wae5eE" name="tribit-stormbox-micro-3-portable-bluetoo-436aec5e-06de-4963-aa5c-3abc29a65587.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/LDHzrLzgPGmoS2J4wae5eE.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I love my Tribit Stormbox Micro 3. This palm-sized speaker brings huge sound, with customizable EQ, 24 hours of battery life on a charge and easy-to-use controls. It’s also great for taking outdoors with you thanks to its IP68 waterproof rating and drop protection. Best of all — it’s magnetic, so you can mount it on any magnetic surface and listen hands-free.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="d0e7e9c4-8c00-11f1-a222-9ba87d24e3f3" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$64">View Deal</a></p></div><p>The Tribit Stormbox Micro 3 is so small that it easily fits into my palm. But it gets amazingly loud. Cranked up to full volume, I can hear it through my whole house, and potentially bother my neighbors. The sound is detailed, too. Every track I listened to felt like it had more layers and dimension through this speaker. My phone’s built-in speakers sound tinny and thin in comparison; I just can’t go back to listening to music the old way.</p><p>This mini speaker is also incredibly convenient to take around. There’s a flexible built-in strap that’ll wrap around a range of objects. It’s ideal for attaching to a bike handle or backpack. Alternatively, it has a built-in magnet, so when I’m in the house, I take it with me everywhere, because I can stick it on any magnetic surface with ease. Cooking in the kitchen? Stick it on the fridge door. My desk is extremely messy, but I can save space on the desktop by sticking it to my desk’s metal leg.</p><p>The battery life is excellent and outperforms rivals like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> (10 hours.) It’s rated for 24 hours of playtime at medium volume, so you can keep the party going all day and into the night. Personally, I’ve only charged it once since I got it and it’s still going strong. That’s pretty impressive. And not only does it last for a long time, it can also be used as a power bank to charge your phone or other devices in a pinch. </p><p>The speaker has a cool, modern look and comes in three color options — black, blue and gray. Can we get pink as an option next, please? The good design extends to the controls, which are easy to use. You can control volume and playback with the large face buttons, while other features like Bluetooth pairing and Bass Boost have dedicated buttons on the side.</p><p>Don’t stick with bad sound quality — grab yourself a Tribit Stormbox Micro 3 ASAP.</p><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Everything&rows=6&view_mode=savings_squad&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function getTimeAgo(dateString) {        if (!dateString) return '';        const date = new Date(dateString);        const now = new Date();        const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);        if (diffInSeconds < 60) return 'Just now';        const diffInMinutes = Math.floor(diffInSeconds / 60);        if (diffInMinutes < 60) return `${diffInMinutes} min${diffInMinutes > 1 ? 's' : ''} ago`;        const diffInHours = Math.floor(diffInMinutes / 60);        if (diffInHours < 24) return `${diffInHours} hr${diffInHours > 1 ? 's' : ''} ago`;        const diffInDays = Math.floor(diffInHours / 24);        if (diffInDays < 30) return `${diffInDays} day${diffInDays > 1 ? 's' : ''} ago`;        const diffInMonths = Math.floor(diffInDays / 30);        if (diffInMonths < 12) return `${diffInMonths} mo${diffInMonths > 1 ? 's' : ''} ago`;        const diffInYears = Math.floor(diffInDays / 365);        return `${diffInYears} yr${diffInYears > 1 ? 's' : ''} ago`;      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;      position: relative;      z-index: 20;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      margin: 0 auto;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters-container {      position: relative;      width: 100%;      max-width: 800px;    }    .tg-df-scroll-btn {      display: none;      position: absolute;      top: 50%;      transform: translateY(-50%);      width: 32px;      height: 32px;      background: white;      border: 1px solid var(--tg-df-border);      border-radius: 50%;      align-items: center;      justify-content: center;      cursor: pointer;      z-index: 10;      box-shadow: 0 2px 8px rgba(0,0,0,0.1);      color: var(--tg-df-text-primary);      padding: 0;    }    .tg-df-scroll-btn svg {      width: 16px;      height: 16px;    }    .tg-df-scroll-btn:hover {      background: #f4f4f4;    }    .tg-df-scroll-btn.left {      left: 0px;    }    .tg-df-scroll-btn.right {      right: 0px;    }    @container tg-df (max-width: 768px) {      .tg-df-scroll-btn {        display: flex;        top: 22px; /* vertically center within the 44px high filter buttons */      }    }    .tg-df-filters {      display: grid;      width: 100%;      grid-template-columns: repeat(4, 1fr);      gap: 12px;      margin: 0 auto;      max-width: 800px;    }                 .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;      width: 100%;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      width: 100%;      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }    @container tg-df (max-width: 768px) {      .tg-df-filters-container {      position: relative;      width: 100%;      max-width: 800px;    }    .tg-df-scroll-btn {      display: none;      position: absolute;      top: 50%;      transform: translateY(-50%);      width: 32px;      height: 32px;      background: white;      border: 1px solid var(--tg-df-border);      border-radius: 50%;      align-items: center;      justify-content: center;      cursor: pointer;      z-index: 10;      box-shadow: 0 2px 8px rgba(0,0,0,0.1);      color: var(--tg-df-text-primary);      padding: 0;    }    .tg-df-scroll-btn svg {      width: 16px;      height: 16px;    }    .tg-df-scroll-btn:hover {      background: #f4f4f4;    }    .tg-df-scroll-btn.left {      left: 0px;    }    .tg-df-scroll-btn.right {      right: 0px;    }    @container tg-df (max-width: 768px) {      .tg-df-scroll-btn {        display: flex;        top: 22px; /* vertically center within the 44px high filter buttons */      }    }    .tg-df-filters {        width: 100%;        margin: 0;        margin-bottom: -320px;        padding: 0 16px 320px 16px;        display: flex;        flex-wrap: nowrap;        gap: 8px;        overflow-x: auto;        overflow-y: hidden;        pointer-events: none;        scrollbar-width: none;        -webkit-overflow-scrolling: touch;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }      .tg-df-sort-wrapper {        pointer-events: auto;        flex: 0 0 auto;        width: 175px;        min-width: 175px;      }    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 120px;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 16px 16px 8px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }                  .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;        width: auto;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;      width: 100vw;      max-width: 1200px;      position: relative;      left: 50%;      transform: translateX(-50%);    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 14px;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }        .tg-df-container .tg-df-carousel-scroll-left,    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-left { left: 8px; }    .tg-df-container .tg-df-carousel-scroll-right { right: 8px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left { left: 0px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { right: 0px; }    .tg-df-carousel-filters-outer { margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; }    .tg-df-grid-wrapper { position: relative; }    @container tg-df (max-width: 599px) { .tg-df-carousel-filters-outer { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; } }        .tg-df-container .tg-df-carousel-scroll-left:hover,    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn {      display: inline-flex;      align-items: center;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.40);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #1f69ff; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #0056e0; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }    .tg-df-savings-squad-header { margin-bottom: 24px; text-align: center; display: none; }    .tg-df-banner-img-desktop { display: block; width: 100%; height: auto; margin-bottom: 32px; }    .tg-df-banner-img-mobile { display: none; width: 100%; height: auto; margin-bottom: 32px; }    @container tg-df (max-width: 600px) {      .tg-df-banner-img-desktop { display: none; }      .tg-df-banner-img-mobile { display: block; }    }    .tg-df-header-title { font-size: 28px; font-weight: 700; color: var(--tg-df-text); margin: 32px 0 12px 0; line-height: 1.3; }    .tg-df-header-subtitle { font-size: 16px; color: var(--tg-df-text-muted); margin: 0 0 32px 0; line-height: 1.5; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3Cdiv class="tg-df-savings-squad-header" id="tg-df-savings-squad-header">      \x3Cpicture>        \x3Cimg src="https://cdn.mos.cms.futurecdn.net/flexiimages/xkh2og7m3d1778189998.png" alt="Deals Banner" class="tg-df-banner-img-desktop" />        \x3Cimg src="https://cdn.mos.cms.futurecdn.net/flexiimages/gmak6rtdf41778245089.png" alt="Deals Banner Mobile" class="tg-df-banner-img-mobile" />      \x3C/picture>      \x3Cdiv class="tg-df-header-text">        \x3Ch2 class="tg-df-header-title" id="tg-df-header-title">Editor's Choice Deals\x3C/h2>        \x3Cp class="tg-df-header-subtitle" id="tg-df-header-subtitle">Discover the best discounts currently available, curated daily by the Tom's Guide Savings Squad.\x3C/p>      \x3C/div>    \x3C/div>    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-roundels">                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Televisions" data-pr="all">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/wcMxTsHgqu3roMbAx7RLnT-132-100.png" alt="TVs" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">TVs\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Phones" data-pr="over50">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/G3KGaRGzj24F6PUsw4bWpT-132-100.png" alt="Phones" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Phones\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Computing" data-pr="all">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/znNvsLzx8NEgNkD9HSFSnT-132-100.png" alt="Computing" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Computing\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Gaming" data-pr="all">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/Pgew8yaRQeZFHqHjTzvBnT-132-100.png" alt="Gaming" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Gaming\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Mattresses" data-pr="over500">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/cW7xsaLyesxkHFVSiC4kmT-132-100.png" alt="Mattresses" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Mattresses\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Audio" data-pr="over30">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/pCvBVHuhaQVjKt3VgCjbqT-132-100.png" alt="Audio" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Audio\x3C/span>            \x3C/div>                  \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-outer" style="position: relative;">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>        \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3C/div>    \x3C/div>    \x3C/div>      \x3C!-- Search & Filter Controls --\x3E      \x3Cdiv class="tg-df-top-bar" id="tg-df-top-bar" style="position: relative; z-index: 100; margin: 0 auto 20px;">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>      \x3C/div>    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">              \x3Cdiv class="tg-df-filters-container" style="position: relative; width: 100%; max-width: 800px; margin: 0 auto;">          \x3Cbutton class="tg-df-scroll-btn left" style="display: none;" onclick="this.parentElement.querySelector('.tg-df-filters').scrollBy({left: -200, behavior: 'smooth'})">            \x3Csvg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\x3Cpath d="M15 18l-6-6 6-6"/>\x3C/svg>          \x3C/button>          \x3Cbutton class="tg-df-scroll-btn right" style="display: none;" onclick="this.parentElement.querySelector('.tg-df-filters').scrollBy({left: 200, behavior: 'smooth'})">            \x3Csvg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\x3Cpath d="M9 18l6-6-6-6"/>\x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-filters">          \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>          \x3C/div>        \x3C/div>        \x3C/div>      \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>      \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();        }        getViewMode() {          console.log("DEBUG getViewMode -> override:", this.viewModeOverride, "editorMode:", this.editorMode);          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          console.log("[DEBUG] applyLayoutMode CALLED! mode=", mode);          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          const showHeaderDetails = params.get('show_header_details') !== 'false';          const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          const topBarDiv = this.root.querySelector('#tg-df-top-bar');          const headerElement = this.root.querySelector('#tg-df-savings-squad-header');          if (headerElement) {             headerElement.style.display = (mode === 'savings_squad' && showHeaderDetails) ? 'block' : 'none';          }          if (mode === 'carousel') {             this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (topBarDiv) topBarDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.add('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (topBarDiv) topBarDiv.style.display = 'block';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!this.viewModeOverride && initialViewMode) {            this.viewModeOverride = initialViewMode;          }          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Gaming laptops';          }          const website = params.get('website') || 'tomsguide';          this.website = website;          if (website === 'techradar') {            const squadHeader = this.root.querySelector('.tg-df-savings-squad-header');            if (squadHeader) {               const pic = squadHeader.querySelector('picture');               if (pic) pic.style.display = 'none';            }            const style = document.createElement('style');            style.innerHTML = `              .tg-df-container .hawk-affiliate-link-deal-button, .tg-df-container .tg-df-card-cta { background-color: #5DAF08 !important; }              .tg-df-container .hawk-affiliate-link-deal-button:hover, .tg-df-container .tg-df-card-cta:hover { background-color: #4a8c06 !important; }            `;            this.root.appendChild(style);          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (this.viewModeSelect && params.has('view_mode')) {            this.viewModeSelect.value = params.get('view_mode');          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'date_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }          const headerTitleEl = this.root.querySelector('#tg-df-header-title');          const headerSubtitleEl = this.root.querySelector('#tg-df-header-subtitle');          if (params.has('widget_title') && headerTitleEl) {             headerTitleEl.textContent = params.get('widget_title');          }          if (params.has('widget_subtitle') && headerSubtitleEl) {             headerSubtitleEl.textContent = params.get('widget_subtitle');          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            if (this.getViewMode() === 'savings_squad') {              this.searchInput.value = '';            } else {              this.searchInput.value = this.currentQuery;            }          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'date_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const handleFiltersScroll = () => {            const filters = this.root.querySelector('.tg-df-filters');            const leftBtn = this.root.querySelector('.tg-df-scroll-btn.left');            const rightBtn = this.root.querySelector('.tg-df-scroll-btn.right');            if (filters && leftBtn && rightBtn) {              const { scrollLeft, scrollWidth, clientWidth } = filters;              leftBtn.style.display = scrollLeft > 0 ? 'flex' : 'none';              rightBtn.style.display = Math.ceil(scrollLeft + clientWidth) < scrollWidth - 5 ? 'flex' : 'none';            }          };          const filters = this.root.querySelector('.tg-df-filters');          if (filters) {            filters.addEventListener('scroll', handleFiltersScroll);            window.addEventListener('resize', handleFiltersScroll);            setTimeout(handleFiltersScroll, 100);                        // Also call after rendering dropdowns            const origRenderCategories = this.renderCategories;            if (origRenderCategories) {               this.renderCategories = (...args) => {                 origRenderCategories.apply(this, args);                 setTimeout(handleFiltersScroll, 50);               };            }          }                const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          const showAutocomplete = () => {             if (this.getViewMode() !== 'savings_squad' || !this.autocompleteDropdown || !this.airedaleTags) return;                          let terms = this.airedaleTags;             if (this.airedaleBrands) {                terms = terms.concat(this.airedaleBrands.map(b => b.formatted_value));             }             terms = [...new Set(terms)];                          const query = this.searchInput.value.trim();             let matches = [];             if (query.length > 0) {                 matches = terms.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase());             } else {                 matches = terms;             }                          if (matches.length > 0) {                 this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                 this.autocompleteDropdown.classList.add('active');             } else {                 this.autocompleteDropdown.classList.remove('active');             }          };          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('focus', showAutocomplete);            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              showAutocomplete();              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  if (this.getViewMode() === 'savings_squad') {                    this.activeDealTag = null;                    this.currentQuery = '';                    if (this.categoryFilter) this.categoryFilter.value = 'all';                    this.fetchDeals('');                  } else {                    this.deals = [];                    this.render();                  }                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                                let isTag = false;                if (this.airedaleTags && this.airedaleTags.includes(query)) isTag = true;                if (this.airedaleBrands && this.airedaleBrands.some(b => b.formatted_value === query)) isTag = true;                this.activeDealTag = isTag ? query : null;                                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   if (query.length === 0 && this.getViewMode() === 'savings_squad') {                       if (this.categoryFilter) this.categoryFilter.value = 'all';                   }                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   if (this.categoryFilter && this.airedaleTags.includes(tag)) {                       this.categoryFilter.value = tag;                   }                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                            let isTag = false;              if (this.airedaleTags && this.airedaleTags.includes(query)) isTag = true;              if (this.airedaleBrands && this.airedaleBrands.some(b => b.formatted_value === query)) isTag = true;              this.activeDealTag = isTag ? query : null;                            this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 if (query.length === 0 && this.getViewMode() === 'savings_squad') {                     if (this.categoryFilter) this.categoryFilter.value = 'all';                 }                 this.fetchDeals(query);              }            });          }          if(this.sortSelect && this.sortSelect.querySelector('option[value="date_desc"]') === null) {              const option = document.createElement('option');              option.value = "date_desc";              option.text = "Newest First";              this.sortSelect.insertBefore(option, this.sortSelect.firstChild);          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               if (val) {                 this.currentQuery = val;               } else {                 if (this.searchInput && this.currentQuery === document.querySelector('#tg-df-brand-trigger')?.getAttribute('data-active-brand')) {                     // don't clear current query if a brand is selected                 } else if (this.searchInput) {                     this.currentQuery = '';                     this.searchInput.value = '';                 }               }               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = 'date_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }          this.setupScrollListeners();        }        setupScrollListeners() {          const containers = [             this.root.querySelector('.tg-df-carousel-roundels'),             this.root.querySelector('.tg-df-carousel-filters-wrap'),             this.root.querySelector('#tg-df-grid')          ];                    containers.forEach(container => {             if (!container) return;                          const checkScroll = () => {                if (!container.parentElement) return;                const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left');                const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right');                                if (leftBtn) {                   if (container.scrollLeft <= 5) leftBtn.style.display = 'none';                   else leftBtn.style.display = 'flex';                }                                if (rightBtn) {                   if (container.scrollWidth <= container.clientWidth) {                       rightBtn.style.display = 'none';                   } else if (container.scrollLeft >= container.scrollWidth - container.clientWidth - 5) {                       rightBtn.style.display = 'none';                   } else {                       rightBtn.style.display = 'flex';                   }                }             };                          container.addEventListener('scroll', checkScroll);             checkScroll();                          window.addEventListener('resize', checkScroll);                          const observer = new MutationObserver(checkScroll);             observer.observe(container, { childList: true, subtree: true, characterData: false });          });        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getRowsLimit() {          if (this.rowsSelect && this.rowsSelect.value) return parseInt(this.rowsSelect.value, 10);          let configSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {             configSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {             configSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(configSource);          if (params.has('rows')) return parseInt(params.get('rows'), 10) || 12;          return 12;        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }        async fetchDeals(query) {          this.showLoading();          this.deals = [];          this.displayLimit = this.getRowsLimit();                    try {            console.log("getViewMode returns:", this.getViewMode());            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad();            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query);               } else {                 await this.fetchHawkDeals(query);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const siteParam = this.website || 'tomsguide';            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=${siteParam}&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=${siteParam}&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (!this.activeDealTag && this.currentQuery) {             const tagMatch = this.airedaleTags.find(t => t.toLowerCase() === this.currentQuery.toLowerCase());             if (tagMatch) {                this.activeDealTag = tagMatch;             }          }          if (this.activeDealTag) {             const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');             const encodedTag = encodeURIComponent(cleanTag);             const siteParam = this.website || 'tomsguide';             const url = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=${siteParam}&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let seenUrls = new Set();                    let overallBrandsCounts = {};                    // First pass: extract ALL brands from topArticles so the dropdown has all options          topArticles.forEach((article) => {             if (!article.articlepage) return;             let pageData = [];             try { pageData = JSON.parse(article.articlepage[0]); } catch(e){ console.warn(e); }             const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');             savingsSquad.forEach((block) => {                const data = block.data || {};                if (data.brand) {                   const cleanBrand = data.brand.replace(/^\d+\.\s*/, '').trim();                   overallBrandsCounts[cleanBrand] = (overallBrandsCounts[cleanBrand] || 0) + 1;                }             });          });          targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                if (externalUrl) {                   if (seenUrls.has(externalUrl)) return;                  seenUrls.add(externalUrl);                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || '',                   authorName: article.articleauthortext ? article.articleauthortext[0] : (article.articleauthor ? article.articleauthor[0] : ''),                   authorRole: article.articleauthorrole ? article.articleauthorrole[0] : '',                   authorImage: article.articleauthormedia ? article.articleauthormedia[0] : '',                   documentUrl: article.documenturl ? article.documenturl[0] : '',                   modifiedDate: article.contentmodifieddate || article.modifieddate || ''                });             });          });                    const airedaleBrandsList = Object.keys(overallBrandsCounts).map(b => ({              formatted_value: b,              count: overallBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    url.searchParams.append('rows', this.getRowsLimit().toString());          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = this.getRowsLimit();          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : 'date_desc';          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                ${_div}              ${_div}              \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin:0; border-radius:0;">${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map((v, idx) => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              const revenueIdVal = generateRevenueId(linkUrl, offerName, merchantName, null);              const rewrittenLinkUrl = rewriteAffiliateLink(linkUrl, area, revenueIdVal);              return `                \x3Ca href="${this.escapeHTML(rewrittenLinkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item"                  data-action="voucher-click"                  data-product-name="${this.escapeHTML(offerName)}"                  data-merchant-name="${this.escapeHTML(merchantName)}"                  data-analytics-id="${this.escapeHTML(offerObj.offer_id || offerObj.id || v.id || '')}"                  data-price=""                  data-previous-price=""                  data-original-link="${this.escapeHTML(linkUrl)}"                  data-revenue-id="${revenueIdVal}"                  data-index="${idx}"                  data-total="${offers.length}"                  data-in-stock="true"                  data-currency="USD"                  data-model-id="${this.escapeHTML(offerObj.model_id || v.model_id || offerObj.id || v.id || '')}"                  data-merchant-id="${this.escapeHTML(offerObj.merchant_id || offerObj.merchant?.id || '')}"                >                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });            // Attach voucher click tracking            const voucherBtns = content.querySelectorAll('[data-action="voucher-click"]');            voucherBtns.forEach(btn => {              btn.addEventListener('click', (e) => {                if (e.target.closest('[data-action="copy-code"]')) return;                                const productName = btn.getAttribute('data-product-name');                const merchantNameAttr = btn.getAttribute('data-merchant-name');                const productId = btn.getAttribute('data-analytics-id');                const price = parseFloat(btn.getAttribute('data-price')) || null;                const prevPriceStr = btn.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = btn.getAttribute('data-original-link');                const rewrittenLink = btn.getAttribute('href');                const revenueId = btn.getAttribute('data-revenue-id');                const index = parseInt(btn.getAttribute('data-index'), 10) || 0;                const inStock = btn.getAttribute('data-in-stock') === 'true';                const totalText = btn.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: btn.getAttribute('data-model-id') || null,                    matchId: btn.getAttribute('data-match-id') || null,                    brand: btn.getAttribute('data-model-brand') || null,                    parent: btn.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: btn.getAttribute('data-merchant-id') || null,                    network: btn.getAttribute('data-merchant-network') || null,                    url: btn.getAttribute('data-merchant-url') || null,                    name: merchantNameAttr                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: btn.getAttribute('data-currency') || 'USD'                };                if (typeof trackDealClick === 'function') {                  trackDealClick(trackingParams);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)}${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = 'View Deal';              if (isSavingsSquadMode) {                priceGroupHtml = ``;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = isSavingsSquadMode ? `` : `                \x3Cdiv class="tg-df-card-price-group">                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description tg-df-card-desc-container" style="margin-bottom: 12px; position: relative;">                          \x3Cp class="tg-df-card-desc-content" style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 0; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${this.escapeHTML(deal.description)}${_p}                          \x3Cbutton type="button" class="tg-df-card-desc-btn" style="display: none; appearance: none; border: none; color: #000000; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; font-family: inherit; position: absolute; bottom: 2px; right: 0; background: linear-gradient(to right, transparent, #fff 20%, #fff); padding: 0 0 0 16px;" onclick="                            var c = this.parentNode;                            if (this.dataset.expanded === 'true') {                              var pd = (c.tagName === 'P') ? c : this.previousElementSibling;                              if (c.tagName === 'P') { c.parentNode.appendChild(this); pd = c; }                              pd.style.display = '-webkit-box';                              pd.style.webkitLineClamp = '3';                              this.textContent = 'READ MORE';                              this.style.position = 'absolute';                              this.style.background = 'linear-gradient(to right, transparent, #fff 20%, #fff)';                              this.style.paddingLeft = '16px';                              this.dataset.expanded = 'false';                            } else {                              var pd = this.previousElementSibling;                              pd.style.display = 'inline';                              pd.style.webkitLineClamp = 'unset';                              this.textContent = 'READ LESS';                              this.style.position = 'static';                              this.style.background = 'transparent';                              this.style.paddingLeft = '4px';                              this.dataset.expanded = 'true';                              pd.appendChild(this);                            }                          ">READ MORE${_button}                        \x3C/div>` : ''}                      ${_div}                    ${_div}                    ${deal.authorName ? `                      \x3Cdiv class="tg-df-author-line-mobile" style="padding: 0 0 12px 0; background: transparent;">                         \x3Cdiv style="display: flex; align-items: center; gap: 12px;">                            ${deal.authorImage ? `\x3Cimg src="${this.escapeHTML(deal.authorImage)}" alt="${this.escapeHTML(deal.authorName)}" class="tg-df-author-img" width="40" height="40" style="border-radius: 50%; object-fit: cover; flex-shrink: 0; aspect-ratio: 1 / 1; min-width: 40px; min-height: 40px;">` : ''}                            \x3Cdiv style="display: flex; flex-direction: column;">                               \x3Cdiv style="font-size: 10px; color: var(--tg-df-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; font-weight: 600;">\x3Cspan style="color: #FF6600;">${this.escapeHTML(deal.merchant)}${_span} deal recommended by:${_div}                               \x3Cdiv style="font-size: 11px; color: var(--tg-df-text); line-height: 1.3;">                                  \x3Cstrong>\x3Ca href="https://www.tomsguide.com/${this.escapeHTML(deal.documentUrl || '').replace(/^\/+/, '')}" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit; border-bottom: 1px dotted var(--tg-df-text-muted);">${this.escapeHTML(deal.authorName)}${_a}${_strong}                                  ${deal.authorRole && !['null', 'nul', 'undefined'].includes(String(deal.authorRole).toLowerCase()) ? ` • ${this.escapeHTML(deal.authorRole)}` : ''}                                  ${deal.modifiedDate ? `\x3Cdiv style="color: var(--tg-df-text-muted); margin-top: 2px;">${getTimeAgo(deal.modifiedDate)}${_div}` : ''}                               ${_div}                            ${_div}                         ${_div}                      ${_div}                    ` : ''}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>View Deal${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                  \x3Cdiv class="tg-df-card-merchant-wrapper" style="position: absolute; bottom: 0; right: 0; background: transparent; padding: 8px 12px; z-index: 10;">                     \x3Cspan class="tg-df-card-merchant-pill" style="text-align: right; margin-bottom: 0;" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cdiv class="tg-df-card-desc-container" style="margin-bottom: 12px; position: relative;">                    \x3Cp class="tg-df-card-desc-content" style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 0; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${this.escapeHTML(deal.description)}${_p}                    \x3Cbutton type="button" class="tg-df-card-desc-btn" style="display: none; appearance: none; border: none; color: #000000; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; font-family: inherit; position: absolute; bottom: 2px; right: 0; background: linear-gradient(to right, transparent, #fff 20%, #fff); padding: 0 0 0 16px;" onclick="                      var c = this.parentNode;                      if (this.dataset.expanded === 'true') {                        var pd = (c.tagName === 'P') ? c : this.previousElementSibling;                        if (c.tagName === 'P') { c.parentNode.appendChild(this); pd = c; }                        pd.style.display = '-webkit-box';                        pd.style.webkitLineClamp = '3';                        this.textContent = 'READ MORE';                        this.style.position = 'absolute';                        this.style.background = 'linear-gradient(to right, transparent, #fff 20%, #fff)';                        this.style.paddingLeft = '16px';                        this.dataset.expanded = 'false';                      } else {                        var pd = this.previousElementSibling;                        pd.style.display = 'inline';                        pd.style.webkitLineClamp = 'unset';                        this.textContent = 'READ LESS';                        this.style.position = 'static';                        this.style.background = 'transparent';                        this.style.paddingLeft = '4px';                        this.dataset.expanded = 'true';                        pd.appendChild(this);                      }                    ">READ MORE${_button}                  \x3C/div>` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${deal.authorName ? `                    \x3Cdiv class="tg-df-author-line-desktop" style="padding: 0 0 ${isSavingsSquadMode ? 0 : 12}px 0;">                       \x3Cdiv style="display: flex; align-items: center; gap: 10px;">                          ${deal.authorImage ? `\x3Cimg src="${this.escapeHTML(deal.authorImage)}" alt="${this.escapeHTML(deal.authorName)}" class="tg-df-author-img" width="36" height="36" style="border-radius: 50%; object-fit: cover; flex-shrink: 0; aspect-ratio: 1 / 1; min-width: 36px; min-height: 36px;">` : ''}                          \x3Cdiv style="display: flex; flex-direction: column;">                             \x3Cdiv style="font-size: 10px; color: var(--tg-df-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; font-weight: 600;">Recommended by:${_div}                             \x3Cdiv style="font-size: 11px; color: var(--tg-df-text); line-height: 1.2;">                                \x3Cstrong>\x3Ca href="https://www.tomsguide.com/${this.escapeHTML(deal.documentUrl || '').replace(/^\/+/, '')}" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit; border-bottom: 1px dotted var(--tg-df-text-muted);">${this.escapeHTML(deal.authorName)}${_a}${_strong}                                ${deal.authorRole && !['null', 'nul', 'undefined'].includes(String(deal.authorRole).toLowerCase()) ? ` • ${this.escapeHTML(deal.authorRole)}` : ''}                                ${deal.modifiedDate ? `\x3Cspan style="color: var(--tg-df-text-muted);"> • ${getTimeAgo(deal.modifiedDate)}${_span}` : ''}                             ${_div}                          ${_div}                       ${_div}                    ${_div}                    ` : ''}                    ${priceGroupHtml}                  ${_div}                ${_div}                \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta" style="text-decoration: none; border-radius: 0;">${ctaText}${_a}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit) {            dealsHtml += `              \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer;">Load More${_button}              ${_div}            `;          }                    this.grid.innerHTML = dealsHtml;          // Inject JSON-LD          try {            let targetNode = this.hostContainer || document.head;            let jsonLdScript = targetNode.querySelector('#tg-df-json-ld-' + this.widgetId);            if (!jsonLdScript) {                jsonLdScript = document.createElement('script');                jsonLdScript.type = 'application/ld+json';                jsonLdScript.id = 'tg-df-json-ld-' + this.widgetId;                targetNode.appendChild(jsonLdScript);            }            const jsonLdData = {              "@context": "https://schema.org",              "@type": "ItemList",              "itemListElement": displayDeals.slice(0, this.displayLimit).map((deal, index) => {                 let isoCurrency = "USD";                 if (deal.currency === '£') isoCurrency = "GBP";                 else if (deal.currency === '€') isoCurrency = "EUR";                 else if (deal.currency === 'A$') isoCurrency = "AUD";                 else if (deal.currency === 'CA$') isoCurrency = "CAD";                 const areaCode = typeof this.getAreaCode === 'function' ? this.getAreaCode() : 'US';                 const revenueId = typeof generateRevenueId === 'function' ? generateRevenueId(deal.url, deal.title, deal.merchant, null) : '';                 const rewrittenLink = typeof rewriteAffiliateLink === 'function' ? rewriteAffiliateLink(deal.url, areaCode, revenueId) : deal.url;                 return {                   "@type": "ListItem",                   "position": index + 1,                   "item": {                     "@type": "Product",                     "name": deal.title,                     "image": deal.image || "",                     "description": deal.description || "",                     "brand": {                       "@type": "Brand",                       "name": deal.brand || ""                     },                     "offers": {                       "@type": "Offer",                       "priceCurrency": isoCurrency,                       "price": deal.rawPrice || 0,                       "url": rewrittenLink,                       "seller": {                         "@type": "Organization",                         "name": deal.merchant || ""                       }                     }                   }                 };              }).filter(item => item.item.name)            };            jsonLdScript.textContent = JSON.stringify(jsonLdData);          } catch(e) { console.warn("JSON-LD generation failed", e); }          setTimeout(() => {            const contents = this.root.querySelectorAll('.tg-df-card-desc-content');            contents.forEach(p => {              if (p.scrollHeight > p.clientHeight || p.scrollHeight > 60) {                if (p.nextElementSibling) {                  p.nextElementSibling.style.display = 'block';                }              }            });                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));          }, 50);          const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              this.displayLimit += this.getRowsLimit();              this.render();            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/i-thought-my-phones-speakers-were-good-but-after-listening-to-this-usd64-bluetooth-speaker-i-can-never-go-back</link>
                                                                            <description>
                            <![CDATA[ The Tribit Stormbox Micro 3 is my favorite Bluetooth speaker — it’s some of the biggest sound I’ve heard in a speaker so small. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">Zz9EV3yf4jE8ZmoHFYGpBf</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/qBv2YZ4TYdULv5czpLmQTK-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 30 Jul 2026 14:21:19 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ millie.daviswilliams@futurenet.com (Millie Davis-Williams) ]]></author>                    <dc:creator><![CDATA[ Millie Davis-Williams ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/WU6pa7hKuxs5ScCoqcXYWA.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Millie is a Deals Editor at Tom&#039;s Guide specializing in tech deals content. Based in London, Millie has spent a lifetime stubbornly refusing to pay full price on any possible purchase. She has years of searching for the best discounts on the latest and greatest phones, TVs and gadgets under her belt, and knows how to sort the best deals from the rest. Helping readers save on the products they want is her mission, whether it’s an Apple Watch or a new tablet.&lt;/p&gt;&lt;p&gt;Since working at Tom’s Guide, Millie has covered retail events like Prime Day, Black Friday and Cyber Monday extensively, meaning she knows the products readers are looking for and the best times of year to hit “purchase.”&lt;/p&gt;&lt;p&gt;When she’s not looking for discounts, Millie covers news and writes how-to articles about everything from phones, streaming devices, and headphones to apps and video games.&lt;/p&gt;&lt;p&gt;In her spare time, Millie enjoys reading, gaming on her Nintendo Switch and creating digital art. She’s also a fan of watching Disney movies and collecting plush (the latter having totally taken over her bedroom.)&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/qBv2YZ4TYdULv5czpLmQTK-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Tribit Stormbox Micro 3 Bluetooth speaker in hand]]></media:description>                                                            <media:text><![CDATA[Tribit Stormbox Micro 3 Bluetooth speaker in hand]]></media:text>
                                <media:title type="plain"><![CDATA[Tribit Stormbox Micro 3 Bluetooth speaker in hand]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/qBv2YZ4TYdULv5czpLmQTK-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>This isn’t the first time I’ve written about the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit Stormbox Micro 3</a>, and it probably won’t be the last. It’s the best-sounding Bluetooth speaker I’ve tried, and it delivers amazingly big sound considering its small size. I didn’t think I was that big of a music fan, but this Bluetooth speaker has me totally converted.</p><p>Right now you can pick up the <a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">Tribit Stormbox Micro 3 for $64 at Amazon</a>. Every now and then it goes on sale for $49 or less, so keep an eye out for deals! </p><div class="product"><a data-dimension112="d0e7e9c4-8c00-11f1-a222-9ba87d24e3f3" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$64" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="LDHzrLzgPGmoS2J4wae5eE" name="tribit-stormbox-micro-3-portable-bluetoo-436aec5e-06de-4963-aa5c-3abc29a65587.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/LDHzrLzgPGmoS2J4wae5eE.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I love my Tribit Stormbox Micro 3. This palm-sized speaker brings huge sound, with customizable EQ, 24 hours of battery life on a charge and easy-to-use controls. It’s also great for taking outdoors with you thanks to its IP68 waterproof rating and drop protection. Best of all — it’s magnetic, so you can mount it on any magnetic surface and listen hands-free.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="d0e7e9c4-8c00-11f1-a222-9ba87d24e3f3" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$64">View Deal</a></p></div><p>The Tribit Stormbox Micro 3 is so small that it easily fits into my palm. But it gets amazingly loud. Cranked up to full volume, I can hear it through my whole house, and potentially bother my neighbors. The sound is detailed, too. Every track I listened to felt like it had more layers and dimension through this speaker. My phone’s built-in speakers sound tinny and thin in comparison; I just can’t go back to listening to music the old way.</p><p>This mini speaker is also incredibly convenient to take around. There’s a flexible built-in strap that’ll wrap around a range of objects. It’s ideal for attaching to a bike handle or backpack. Alternatively, it has a built-in magnet, so when I’m in the house, I take it with me everywhere, because I can stick it on any magnetic surface with ease. Cooking in the kitchen? Stick it on the fridge door. My desk is extremely messy, but I can save space on the desktop by sticking it to my desk’s metal leg.</p><p>The battery life is excellent and outperforms rivals like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> (10 hours.) It’s rated for 24 hours of playtime at medium volume, so you can keep the party going all day and into the night. Personally, I’ve only charged it once since I got it and it’s still going strong. That’s pretty impressive. And not only does it last for a long time, it can also be used as a power bank to charge your phone or other devices in a pinch. </p><p>The speaker has a cool, modern look and comes in three color options — black, blue and gray. Can we get pink as an option next, please? The good design extends to the controls, which are easy to use. You can control volume and playback with the large face buttons, while other features like Bluetooth pairing and Bass Boost have dedicated buttons on the side.</p><p>Don’t stick with bad sound quality — grab yourself a Tribit Stormbox Micro 3 ASAP.</p><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Everything&rows=6&view_mode=savings_squad&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function getTimeAgo(dateString) {        if (!dateString) return '';        const date = new Date(dateString);        const now = new Date();        const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);        if (diffInSeconds < 60) return 'Just now';        const diffInMinutes = Math.floor(diffInSeconds / 60);        if (diffInMinutes < 60) return `${diffInMinutes} min${diffInMinutes > 1 ? 's' : ''} ago`;        const diffInHours = Math.floor(diffInMinutes / 60);        if (diffInHours < 24) return `${diffInHours} hr${diffInHours > 1 ? 's' : ''} ago`;        const diffInDays = Math.floor(diffInHours / 24);        if (diffInDays < 30) return `${diffInDays} day${diffInDays > 1 ? 's' : ''} ago`;        const diffInMonths = Math.floor(diffInDays / 30);        if (diffInMonths < 12) return `${diffInMonths} mo${diffInMonths > 1 ? 's' : ''} ago`;        const diffInYears = Math.floor(diffInDays / 365);        return `${diffInYears} yr${diffInYears > 1 ? 's' : ''} ago`;      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;      position: relative;      z-index: 20;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      margin: 0 auto;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters-container {      position: relative;      width: 100%;      max-width: 800px;    }    .tg-df-scroll-btn {      display: none;      position: absolute;      top: 50%;      transform: translateY(-50%);      width: 32px;      height: 32px;      background: white;      border: 1px solid var(--tg-df-border);      border-radius: 50%;      align-items: center;      justify-content: center;      cursor: pointer;      z-index: 10;      box-shadow: 0 2px 8px rgba(0,0,0,0.1);      color: var(--tg-df-text-primary);      padding: 0;    }    .tg-df-scroll-btn svg {      width: 16px;      height: 16px;    }    .tg-df-scroll-btn:hover {      background: #f4f4f4;    }    .tg-df-scroll-btn.left {      left: 0px;    }    .tg-df-scroll-btn.right {      right: 0px;    }    @container tg-df (max-width: 768px) {      .tg-df-scroll-btn {        display: flex;        top: 22px; /* vertically center within the 44px high filter buttons */      }    }    .tg-df-filters {      display: grid;      width: 100%;      grid-template-columns: repeat(4, 1fr);      gap: 12px;      margin: 0 auto;      max-width: 800px;    }                 .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;      width: 100%;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      width: 100%;      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }    @container tg-df (max-width: 768px) {      .tg-df-filters-container {      position: relative;      width: 100%;      max-width: 800px;    }    .tg-df-scroll-btn {      display: none;      position: absolute;      top: 50%;      transform: translateY(-50%);      width: 32px;      height: 32px;      background: white;      border: 1px solid var(--tg-df-border);      border-radius: 50%;      align-items: center;      justify-content: center;      cursor: pointer;      z-index: 10;      box-shadow: 0 2px 8px rgba(0,0,0,0.1);      color: var(--tg-df-text-primary);      padding: 0;    }    .tg-df-scroll-btn svg {      width: 16px;      height: 16px;    }    .tg-df-scroll-btn:hover {      background: #f4f4f4;    }    .tg-df-scroll-btn.left {      left: 0px;    }    .tg-df-scroll-btn.right {      right: 0px;    }    @container tg-df (max-width: 768px) {      .tg-df-scroll-btn {        display: flex;        top: 22px; /* vertically center within the 44px high filter buttons */      }    }    .tg-df-filters {        width: 100%;        margin: 0;        margin-bottom: -320px;        padding: 0 16px 320px 16px;        display: flex;        flex-wrap: nowrap;        gap: 8px;        overflow-x: auto;        overflow-y: hidden;        pointer-events: none;        scrollbar-width: none;        -webkit-overflow-scrolling: touch;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }      .tg-df-sort-wrapper {        pointer-events: auto;        flex: 0 0 auto;        width: 175px;        min-width: 175px;      }    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 120px;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 16px 16px 8px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }                  .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;        width: auto;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;      width: 100vw;      max-width: 1200px;      position: relative;      left: 50%;      transform: translateX(-50%);    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 14px;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }        .tg-df-container .tg-df-carousel-scroll-left,    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-left { left: 8px; }    .tg-df-container .tg-df-carousel-scroll-right { right: 8px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left { left: 0px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { right: 0px; }    .tg-df-carousel-filters-outer { margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; }    .tg-df-grid-wrapper { position: relative; }    @container tg-df (max-width: 599px) { .tg-df-carousel-filters-outer { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; } }        .tg-df-container .tg-df-carousel-scroll-left:hover,    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn {      display: inline-flex;      align-items: center;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.40);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #1f69ff; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #0056e0; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }    .tg-df-savings-squad-header { margin-bottom: 24px; text-align: center; display: none; }    .tg-df-banner-img-desktop { display: block; width: 100%; height: auto; margin-bottom: 32px; }    .tg-df-banner-img-mobile { display: none; width: 100%; height: auto; margin-bottom: 32px; }    @container tg-df (max-width: 600px) {      .tg-df-banner-img-desktop { display: none; }      .tg-df-banner-img-mobile { display: block; }    }    .tg-df-header-title { font-size: 28px; font-weight: 700; color: var(--tg-df-text); margin: 32px 0 12px 0; line-height: 1.3; }    .tg-df-header-subtitle { font-size: 16px; color: var(--tg-df-text-muted); margin: 0 0 32px 0; line-height: 1.5; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3Cdiv class="tg-df-savings-squad-header" id="tg-df-savings-squad-header">      \x3Cpicture>        \x3Cimg src="https://cdn.mos.cms.futurecdn.net/flexiimages/xkh2og7m3d1778189998.png" alt="Deals Banner" class="tg-df-banner-img-desktop" />        \x3Cimg src="https://cdn.mos.cms.futurecdn.net/flexiimages/gmak6rtdf41778245089.png" alt="Deals Banner Mobile" class="tg-df-banner-img-mobile" />      \x3C/picture>      \x3Cdiv class="tg-df-header-text">        \x3Ch2 class="tg-df-header-title" id="tg-df-header-title">Editor's Choice Deals\x3C/h2>        \x3Cp class="tg-df-header-subtitle" id="tg-df-header-subtitle">Discover the best discounts currently available, curated daily by the Tom's Guide Savings Squad.\x3C/p>      \x3C/div>    \x3C/div>    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-roundels">                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Televisions" data-pr="all">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/wcMxTsHgqu3roMbAx7RLnT-132-100.png" alt="TVs" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">TVs\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Phones" data-pr="over50">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/G3KGaRGzj24F6PUsw4bWpT-132-100.png" alt="Phones" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Phones\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Computing" data-pr="all">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/znNvsLzx8NEgNkD9HSFSnT-132-100.png" alt="Computing" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Computing\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Gaming" data-pr="all">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/Pgew8yaRQeZFHqHjTzvBnT-132-100.png" alt="Gaming" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Gaming\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Mattresses" data-pr="over500">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/cW7xsaLyesxkHFVSiC4kmT-132-100.png" alt="Mattresses" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Mattresses\x3C/span>            \x3C/div>                      \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Audio" data-pr="over30">              \x3Cdiv class="tg-df-roundel-img-box">                 \x3Cimg src="https://cdn.mos.cms.futurecdn.net/pCvBVHuhaQVjKt3VgCjbqT-132-100.png" alt="Audio" />              \x3C/div>              \x3Cspan class="tg-df-roundel-label">Audio\x3C/span>            \x3C/div>                  \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-outer" style="position: relative;">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>        \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3C/div>    \x3C/div>    \x3C/div>      \x3C!-- Search & Filter Controls --\x3E      \x3Cdiv class="tg-df-top-bar" id="tg-df-top-bar" style="position: relative; z-index: 100; margin: 0 auto 20px;">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>      \x3C/div>    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">              \x3Cdiv class="tg-df-filters-container" style="position: relative; width: 100%; max-width: 800px; margin: 0 auto;">          \x3Cbutton class="tg-df-scroll-btn left" style="display: none;" onclick="this.parentElement.querySelector('.tg-df-filters').scrollBy({left: -200, behavior: 'smooth'})">            \x3Csvg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\x3Cpath d="M15 18l-6-6 6-6"/>\x3C/svg>          \x3C/button>          \x3Cbutton class="tg-df-scroll-btn right" style="display: none;" onclick="this.parentElement.querySelector('.tg-df-filters').scrollBy({left: 200, behavior: 'smooth'})">            \x3Csvg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\x3Cpath d="M9 18l6-6-6-6"/>\x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-filters">          \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>          \x3C/div>        \x3C/div>        \x3C/div>      \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>      \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();        }        getViewMode() {          console.log("DEBUG getViewMode -> override:", this.viewModeOverride, "editorMode:", this.editorMode);          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          console.log("[DEBUG] applyLayoutMode CALLED! mode=", mode);          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          const showHeaderDetails = params.get('show_header_details') !== 'false';          const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          const topBarDiv = this.root.querySelector('#tg-df-top-bar');          const headerElement = this.root.querySelector('#tg-df-savings-squad-header');          if (headerElement) {             headerElement.style.display = (mode === 'savings_squad' && showHeaderDetails) ? 'block' : 'none';          }          if (mode === 'carousel') {             this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (topBarDiv) topBarDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.add('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (topBarDiv) topBarDiv.style.display = 'block';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!this.viewModeOverride && initialViewMode) {            this.viewModeOverride = initialViewMode;          }          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Gaming laptops';          }          const website = params.get('website') || 'tomsguide';          this.website = website;          if (website === 'techradar') {            const squadHeader = this.root.querySelector('.tg-df-savings-squad-header');            if (squadHeader) {               const pic = squadHeader.querySelector('picture');               if (pic) pic.style.display = 'none';            }            const style = document.createElement('style');            style.innerHTML = `              .tg-df-container .hawk-affiliate-link-deal-button, .tg-df-container .tg-df-card-cta { background-color: #5DAF08 !important; }              .tg-df-container .hawk-affiliate-link-deal-button:hover, .tg-df-container .tg-df-card-cta:hover { background-color: #4a8c06 !important; }            `;            this.root.appendChild(style);          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (this.viewModeSelect && params.has('view_mode')) {            this.viewModeSelect.value = params.get('view_mode');          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'date_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }          const headerTitleEl = this.root.querySelector('#tg-df-header-title');          const headerSubtitleEl = this.root.querySelector('#tg-df-header-subtitle');          if (params.has('widget_title') && headerTitleEl) {             headerTitleEl.textContent = params.get('widget_title');          }          if (params.has('widget_subtitle') && headerSubtitleEl) {             headerSubtitleEl.textContent = params.get('widget_subtitle');          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            if (this.getViewMode() === 'savings_squad') {              this.searchInput.value = '';            } else {              this.searchInput.value = this.currentQuery;            }          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'date_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const handleFiltersScroll = () => {            const filters = this.root.querySelector('.tg-df-filters');            const leftBtn = this.root.querySelector('.tg-df-scroll-btn.left');            const rightBtn = this.root.querySelector('.tg-df-scroll-btn.right');            if (filters && leftBtn && rightBtn) {              const { scrollLeft, scrollWidth, clientWidth } = filters;              leftBtn.style.display = scrollLeft > 0 ? 'flex' : 'none';              rightBtn.style.display = Math.ceil(scrollLeft + clientWidth) < scrollWidth - 5 ? 'flex' : 'none';            }          };          const filters = this.root.querySelector('.tg-df-filters');          if (filters) {            filters.addEventListener('scroll', handleFiltersScroll);            window.addEventListener('resize', handleFiltersScroll);            setTimeout(handleFiltersScroll, 100);                        // Also call after rendering dropdowns            const origRenderCategories = this.renderCategories;            if (origRenderCategories) {               this.renderCategories = (...args) => {                 origRenderCategories.apply(this, args);                 setTimeout(handleFiltersScroll, 50);               };            }          }                const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          const showAutocomplete = () => {             if (this.getViewMode() !== 'savings_squad' || !this.autocompleteDropdown || !this.airedaleTags) return;                          let terms = this.airedaleTags;             if (this.airedaleBrands) {                terms = terms.concat(this.airedaleBrands.map(b => b.formatted_value));             }             terms = [...new Set(terms)];                          const query = this.searchInput.value.trim();             let matches = [];             if (query.length > 0) {                 matches = terms.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase());             } else {                 matches = terms;             }                          if (matches.length > 0) {                 this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                 this.autocompleteDropdown.classList.add('active');             } else {                 this.autocompleteDropdown.classList.remove('active');             }          };          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('focus', showAutocomplete);            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              showAutocomplete();              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  if (this.getViewMode() === 'savings_squad') {                    this.activeDealTag = null;                    this.currentQuery = '';                    if (this.categoryFilter) this.categoryFilter.value = 'all';                    this.fetchDeals('');                  } else {                    this.deals = [];                    this.render();                  }                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                                let isTag = false;                if (this.airedaleTags && this.airedaleTags.includes(query)) isTag = true;                if (this.airedaleBrands && this.airedaleBrands.some(b => b.formatted_value === query)) isTag = true;                this.activeDealTag = isTag ? query : null;                                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   if (query.length === 0 && this.getViewMode() === 'savings_squad') {                       if (this.categoryFilter) this.categoryFilter.value = 'all';                   }                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   if (this.categoryFilter && this.airedaleTags.includes(tag)) {                       this.categoryFilter.value = tag;                   }                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                            let isTag = false;              if (this.airedaleTags && this.airedaleTags.includes(query)) isTag = true;              if (this.airedaleBrands && this.airedaleBrands.some(b => b.formatted_value === query)) isTag = true;              this.activeDealTag = isTag ? query : null;                            this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 if (query.length === 0 && this.getViewMode() === 'savings_squad') {                     if (this.categoryFilter) this.categoryFilter.value = 'all';                 }                 this.fetchDeals(query);              }            });          }          if(this.sortSelect && this.sortSelect.querySelector('option[value="date_desc"]') === null) {              const option = document.createElement('option');              option.value = "date_desc";              option.text = "Newest First";              this.sortSelect.insertBefore(option, this.sortSelect.firstChild);          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               if (val) {                 this.currentQuery = val;               } else {                 if (this.searchInput && this.currentQuery === document.querySelector('#tg-df-brand-trigger')?.getAttribute('data-active-brand')) {                     // don't clear current query if a brand is selected                 } else if (this.searchInput) {                     this.currentQuery = '';                     this.searchInput.value = '';                 }               }               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = 'date_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }          this.setupScrollListeners();        }        setupScrollListeners() {          const containers = [             this.root.querySelector('.tg-df-carousel-roundels'),             this.root.querySelector('.tg-df-carousel-filters-wrap'),             this.root.querySelector('#tg-df-grid')          ];                    containers.forEach(container => {             if (!container) return;                          const checkScroll = () => {                if (!container.parentElement) return;                const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left');                const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right');                                if (leftBtn) {                   if (container.scrollLeft <= 5) leftBtn.style.display = 'none';                   else leftBtn.style.display = 'flex';                }                                if (rightBtn) {                   if (container.scrollWidth <= container.clientWidth) {                       rightBtn.style.display = 'none';                   } else if (container.scrollLeft >= container.scrollWidth - container.clientWidth - 5) {                       rightBtn.style.display = 'none';                   } else {                       rightBtn.style.display = 'flex';                   }                }             };                          container.addEventListener('scroll', checkScroll);             checkScroll();                          window.addEventListener('resize', checkScroll);                          const observer = new MutationObserver(checkScroll);             observer.observe(container, { childList: true, subtree: true, characterData: false });          });        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getRowsLimit() {          if (this.rowsSelect && this.rowsSelect.value) return parseInt(this.rowsSelect.value, 10);          let configSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {             configSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {             configSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(configSource);          if (params.has('rows')) return parseInt(params.get('rows'), 10) || 12;          return 12;        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }        async fetchDeals(query) {          this.showLoading();          this.deals = [];          this.displayLimit = this.getRowsLimit();                    try {            console.log("getViewMode returns:", this.getViewMode());            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad();            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query);               } else {                 await this.fetchHawkDeals(query);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const siteParam = this.website || 'tomsguide';            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=${siteParam}&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=${siteParam}&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (!this.activeDealTag && this.currentQuery) {             const tagMatch = this.airedaleTags.find(t => t.toLowerCase() === this.currentQuery.toLowerCase());             if (tagMatch) {                this.activeDealTag = tagMatch;             }          }          if (this.activeDealTag) {             const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');             const encodedTag = encodeURIComponent(cleanTag);             const siteParam = this.website || 'tomsguide';             const url = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=${siteParam}&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let seenUrls = new Set();                    let overallBrandsCounts = {};                    // First pass: extract ALL brands from topArticles so the dropdown has all options          topArticles.forEach((article) => {             if (!article.articlepage) return;             let pageData = [];             try { pageData = JSON.parse(article.articlepage[0]); } catch(e){ console.warn(e); }             const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');             savingsSquad.forEach((block) => {                const data = block.data || {};                if (data.brand) {                   const cleanBrand = data.brand.replace(/^\d+\.\s*/, '').trim();                   overallBrandsCounts[cleanBrand] = (overallBrandsCounts[cleanBrand] || 0) + 1;                }             });          });          targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                if (externalUrl) {                   if (seenUrls.has(externalUrl)) return;                  seenUrls.add(externalUrl);                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || '',                   authorName: article.articleauthortext ? article.articleauthortext[0] : (article.articleauthor ? article.articleauthor[0] : ''),                   authorRole: article.articleauthorrole ? article.articleauthorrole[0] : '',                   authorImage: article.articleauthormedia ? article.articleauthormedia[0] : '',                   documentUrl: article.documenturl ? article.documenturl[0] : '',                   modifiedDate: article.contentmodifieddate || article.modifieddate || ''                });             });          });                    const airedaleBrandsList = Object.keys(overallBrandsCounts).map(b => ({              formatted_value: b,              count: overallBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    url.searchParams.append('rows', this.getRowsLimit().toString());          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = this.getRowsLimit();          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : 'date_desc';          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                ${_div}              ${_div}              \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin:0; border-radius:0;">${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map((v, idx) => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              const revenueIdVal = generateRevenueId(linkUrl, offerName, merchantName, null);              const rewrittenLinkUrl = rewriteAffiliateLink(linkUrl, area, revenueIdVal);              return `                \x3Ca href="${this.escapeHTML(rewrittenLinkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item"                  data-action="voucher-click"                  data-product-name="${this.escapeHTML(offerName)}"                  data-merchant-name="${this.escapeHTML(merchantName)}"                  data-analytics-id="${this.escapeHTML(offerObj.offer_id || offerObj.id || v.id || '')}"                  data-price=""                  data-previous-price=""                  data-original-link="${this.escapeHTML(linkUrl)}"                  data-revenue-id="${revenueIdVal}"                  data-index="${idx}"                  data-total="${offers.length}"                  data-in-stock="true"                  data-currency="USD"                  data-model-id="${this.escapeHTML(offerObj.model_id || v.model_id || offerObj.id || v.id || '')}"                  data-merchant-id="${this.escapeHTML(offerObj.merchant_id || offerObj.merchant?.id || '')}"                >                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });            // Attach voucher click tracking            const voucherBtns = content.querySelectorAll('[data-action="voucher-click"]');            voucherBtns.forEach(btn => {              btn.addEventListener('click', (e) => {                if (e.target.closest('[data-action="copy-code"]')) return;                                const productName = btn.getAttribute('data-product-name');                const merchantNameAttr = btn.getAttribute('data-merchant-name');                const productId = btn.getAttribute('data-analytics-id');                const price = parseFloat(btn.getAttribute('data-price')) || null;                const prevPriceStr = btn.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = btn.getAttribute('data-original-link');                const rewrittenLink = btn.getAttribute('href');                const revenueId = btn.getAttribute('data-revenue-id');                const index = parseInt(btn.getAttribute('data-index'), 10) || 0;                const inStock = btn.getAttribute('data-in-stock') === 'true';                const totalText = btn.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: btn.getAttribute('data-model-id') || null,                    matchId: btn.getAttribute('data-match-id') || null,                    brand: btn.getAttribute('data-model-brand') || null,                    parent: btn.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: btn.getAttribute('data-merchant-id') || null,                    network: btn.getAttribute('data-merchant-network') || null,                    url: btn.getAttribute('data-merchant-url') || null,                    name: merchantNameAttr                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: btn.getAttribute('data-currency') || 'USD'                };                if (typeof trackDealClick === 'function') {                  trackDealClick(trackingParams);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)}${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = 'View Deal';              if (isSavingsSquadMode) {                priceGroupHtml = ``;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = isSavingsSquadMode ? `` : `                \x3Cdiv class="tg-df-card-price-group">                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description tg-df-card-desc-container" style="margin-bottom: 12px; position: relative;">                          \x3Cp class="tg-df-card-desc-content" style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 0; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${this.escapeHTML(deal.description)}${_p}                          \x3Cbutton type="button" class="tg-df-card-desc-btn" style="display: none; appearance: none; border: none; color: #000000; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; font-family: inherit; position: absolute; bottom: 2px; right: 0; background: linear-gradient(to right, transparent, #fff 20%, #fff); padding: 0 0 0 16px;" onclick="                            var c = this.parentNode;                            if (this.dataset.expanded === 'true') {                              var pd = (c.tagName === 'P') ? c : this.previousElementSibling;                              if (c.tagName === 'P') { c.parentNode.appendChild(this); pd = c; }                              pd.style.display = '-webkit-box';                              pd.style.webkitLineClamp = '3';                              this.textContent = 'READ MORE';                              this.style.position = 'absolute';                              this.style.background = 'linear-gradient(to right, transparent, #fff 20%, #fff)';                              this.style.paddingLeft = '16px';                              this.dataset.expanded = 'false';                            } else {                              var pd = this.previousElementSibling;                              pd.style.display = 'inline';                              pd.style.webkitLineClamp = 'unset';                              this.textContent = 'READ LESS';                              this.style.position = 'static';                              this.style.background = 'transparent';                              this.style.paddingLeft = '4px';                              this.dataset.expanded = 'true';                              pd.appendChild(this);                            }                          ">READ MORE${_button}                        \x3C/div>` : ''}                      ${_div}                    ${_div}                    ${deal.authorName ? `                      \x3Cdiv class="tg-df-author-line-mobile" style="padding: 0 0 12px 0; background: transparent;">                         \x3Cdiv style="display: flex; align-items: center; gap: 12px;">                            ${deal.authorImage ? `\x3Cimg src="${this.escapeHTML(deal.authorImage)}" alt="${this.escapeHTML(deal.authorName)}" class="tg-df-author-img" width="40" height="40" style="border-radius: 50%; object-fit: cover; flex-shrink: 0; aspect-ratio: 1 / 1; min-width: 40px; min-height: 40px;">` : ''}                            \x3Cdiv style="display: flex; flex-direction: column;">                               \x3Cdiv style="font-size: 10px; color: var(--tg-df-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; font-weight: 600;">\x3Cspan style="color: #FF6600;">${this.escapeHTML(deal.merchant)}${_span} deal recommended by:${_div}                               \x3Cdiv style="font-size: 11px; color: var(--tg-df-text); line-height: 1.3;">                                  \x3Cstrong>\x3Ca href="https://www.tomsguide.com/${this.escapeHTML(deal.documentUrl || '').replace(/^\/+/, '')}" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit; border-bottom: 1px dotted var(--tg-df-text-muted);">${this.escapeHTML(deal.authorName)}${_a}${_strong}                                  ${deal.authorRole && !['null', 'nul', 'undefined'].includes(String(deal.authorRole).toLowerCase()) ? ` • ${this.escapeHTML(deal.authorRole)}` : ''}                                  ${deal.modifiedDate ? `\x3Cdiv style="color: var(--tg-df-text-muted); margin-top: 2px;">${getTimeAgo(deal.modifiedDate)}${_div}` : ''}                               ${_div}                            ${_div}                         ${_div}                      ${_div}                    ` : ''}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>View Deal${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                  \x3Cdiv class="tg-df-card-merchant-wrapper" style="position: absolute; bottom: 0; right: 0; background: transparent; padding: 8px 12px; z-index: 10;">                     \x3Cspan class="tg-df-card-merchant-pill" style="text-align: right; margin-bottom: 0;" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cdiv class="tg-df-card-desc-container" style="margin-bottom: 12px; position: relative;">                    \x3Cp class="tg-df-card-desc-content" style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 0; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${this.escapeHTML(deal.description)}${_p}                    \x3Cbutton type="button" class="tg-df-card-desc-btn" style="display: none; appearance: none; border: none; color: #000000; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; font-family: inherit; position: absolute; bottom: 2px; right: 0; background: linear-gradient(to right, transparent, #fff 20%, #fff); padding: 0 0 0 16px;" onclick="                      var c = this.parentNode;                      if (this.dataset.expanded === 'true') {                        var pd = (c.tagName === 'P') ? c : this.previousElementSibling;                        if (c.tagName === 'P') { c.parentNode.appendChild(this); pd = c; }                        pd.style.display = '-webkit-box';                        pd.style.webkitLineClamp = '3';                        this.textContent = 'READ MORE';                        this.style.position = 'absolute';                        this.style.background = 'linear-gradient(to right, transparent, #fff 20%, #fff)';                        this.style.paddingLeft = '16px';                        this.dataset.expanded = 'false';                      } else {                        var pd = this.previousElementSibling;                        pd.style.display = 'inline';                        pd.style.webkitLineClamp = 'unset';                        this.textContent = 'READ LESS';                        this.style.position = 'static';                        this.style.background = 'transparent';                        this.style.paddingLeft = '4px';                        this.dataset.expanded = 'true';                        pd.appendChild(this);                      }                    ">READ MORE${_button}                  \x3C/div>` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${deal.authorName ? `                    \x3Cdiv class="tg-df-author-line-desktop" style="padding: 0 0 ${isSavingsSquadMode ? 0 : 12}px 0;">                       \x3Cdiv style="display: flex; align-items: center; gap: 10px;">                          ${deal.authorImage ? `\x3Cimg src="${this.escapeHTML(deal.authorImage)}" alt="${this.escapeHTML(deal.authorName)}" class="tg-df-author-img" width="36" height="36" style="border-radius: 50%; object-fit: cover; flex-shrink: 0; aspect-ratio: 1 / 1; min-width: 36px; min-height: 36px;">` : ''}                          \x3Cdiv style="display: flex; flex-direction: column;">                             \x3Cdiv style="font-size: 10px; color: var(--tg-df-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; font-weight: 600;">Recommended by:${_div}                             \x3Cdiv style="font-size: 11px; color: var(--tg-df-text); line-height: 1.2;">                                \x3Cstrong>\x3Ca href="https://www.tomsguide.com/${this.escapeHTML(deal.documentUrl || '').replace(/^\/+/, '')}" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit; border-bottom: 1px dotted var(--tg-df-text-muted);">${this.escapeHTML(deal.authorName)}${_a}${_strong}                                ${deal.authorRole && !['null', 'nul', 'undefined'].includes(String(deal.authorRole).toLowerCase()) ? ` • ${this.escapeHTML(deal.authorRole)}` : ''}                                ${deal.modifiedDate ? `\x3Cspan style="color: var(--tg-df-text-muted);"> • ${getTimeAgo(deal.modifiedDate)}${_span}` : ''}                             ${_div}                          ${_div}                       ${_div}                    ${_div}                    ` : ''}                    ${priceGroupHtml}                  ${_div}                ${_div}                \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta" style="text-decoration: none; border-radius: 0;">${ctaText}${_a}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit) {            dealsHtml += `              \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer;">Load More${_button}              ${_div}            `;          }                    this.grid.innerHTML = dealsHtml;          // Inject JSON-LD          try {            let targetNode = this.hostContainer || document.head;            let jsonLdScript = targetNode.querySelector('#tg-df-json-ld-' + this.widgetId);            if (!jsonLdScript) {                jsonLdScript = document.createElement('script');                jsonLdScript.type = 'application/ld+json';                jsonLdScript.id = 'tg-df-json-ld-' + this.widgetId;                targetNode.appendChild(jsonLdScript);            }            const jsonLdData = {              "@context": "https://schema.org",              "@type": "ItemList",              "itemListElement": displayDeals.slice(0, this.displayLimit).map((deal, index) => {                 let isoCurrency = "USD";                 if (deal.currency === '£') isoCurrency = "GBP";                 else if (deal.currency === '€') isoCurrency = "EUR";                 else if (deal.currency === 'A$') isoCurrency = "AUD";                 else if (deal.currency === 'CA$') isoCurrency = "CAD";                 const areaCode = typeof this.getAreaCode === 'function' ? this.getAreaCode() : 'US';                 const revenueId = typeof generateRevenueId === 'function' ? generateRevenueId(deal.url, deal.title, deal.merchant, null) : '';                 const rewrittenLink = typeof rewriteAffiliateLink === 'function' ? rewriteAffiliateLink(deal.url, areaCode, revenueId) : deal.url;                 return {                   "@type": "ListItem",                   "position": index + 1,                   "item": {                     "@type": "Product",                     "name": deal.title,                     "image": deal.image || "",                     "description": deal.description || "",                     "brand": {                       "@type": "Brand",                       "name": deal.brand || ""                     },                     "offers": {                       "@type": "Offer",                       "priceCurrency": isoCurrency,                       "price": deal.rawPrice || 0,                       "url": rewrittenLink,                       "seller": {                         "@type": "Organization",                         "name": deal.merchant || ""                       }                     }                   }                 };              }).filter(item => item.item.name)            };            jsonLdScript.textContent = JSON.stringify(jsonLdData);          } catch(e) { console.warn("JSON-LD generation failed", e); }          setTimeout(() => {            const contents = this.root.querySelectorAll('.tg-df-card-desc-content');            contents.forEach(p => {              if (p.scrollHeight > p.clientHeight || p.scrollHeight > 60) {                if (p.nextElementSibling) {                  p.nextElementSibling.style.display = 'block';                }              }            });                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));          }, 50);          const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              this.displayLimit += this.getRowsLimit();              this.render();            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Turn up the volume with these Tribit Bluetooth speaker deals — starting at $32 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>There are lots of brands vying for your attention in the Bluetooth speaker space right now — so many, in fact, that it can be easy to ignore the lesser known ones in favor of the names you already know. But venturing beyond those obvious brands can reap some rewards, like discovering a range of great value speakers that offer dynamic, punchy audio and deep bass on a budget.</p><p>Such is the case with Tribit, which has become known for producing tough, compact Bluetooth speakers that often clock in at well under $50. These feature-packed speakers don't compromise on sound, despite their generally small size. </p><p>I've picked out six of my favorite Tribit speakers below, models that demonstrate what this great brand is capable of. What's more, four of these models are currently on sale, meaning you can get a great quality new speaker whatever your budget.</p>        <div class="featured_product_block featured_block_hero" data-id="a9a06412-8a8f-11f1-bd25-030df3d6064b">            <a href="https://www.amazon.com/dp/B07594HZ6Y?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Xsound Go Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:61.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/p8i4REtYUwQa3xLLPMDRBH.jpg" alt="Tribit Xsound Go Portable Bluetooth Speaker, 16w, 24h Playtime, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Xsound Go Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a06250-8a8f-11f1-a5d9-afa110733503">            <a href="https://www.amazon.com/dp/B0DSPCW5M5?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Stormbox Mini+ Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/C5nsJL7vL2zjn6R825kj9G.jpg" alt="Tribit Stormbox Mini+ Portable Bluetooth Speaker, 12w, 360° Sound, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Stormbox Mini+ Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a062be-8a8f-11f1-9929-a72ca031d0b8">            <a href="https://www.amazon.com/dp/B0GF6PPS4S?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Stormbox Micro 3 Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/KX6NK73tSzwTeeKkxpt8iG.jpg" alt="Tribit Stormbox Micro 3 Portable Bluetooth Speaker, 13w, Ip68 Waterproof"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Stormbox Micro 3 Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a061ce-8a8f-11f1-8d28-5741ec4d2c75">            <a href="https://www.amazon.com/dp/B0DN5F9BC2?lv=shuf&channelId=500&plpRedirect=mhFallback&th=1" data-model-name="Tribit StormBox Lava Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/JV6Q2uRVo9iY4M86HXumdM.jpg" alt="Tribit StormBox Lava Bluetooth Speaker."></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit StormBox Lava Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a0639a-8a8f-11f1-b9f8-2f0a56318b35">            <a href="https://www.amazon.com/dp/B09PMYTJD2?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Stormbox Blast Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/KveW89RmovWCrw8QtASGpG.jpg" alt="Tribit Stormbox Blast Bluetooth Speaker, 90w Loud Sound, 30h Playtime"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Stormbox Blast Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a0632c-8a8f-11f1-9379-cfed3571fbf9">            <a href="https://www.amazon.com/dp/B0D9Y7N23T?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit StormBox Blast 2 Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/23hEjsPwDLdrapSHYjkkwV.jpg" alt="Tribit StormBox Blast 2 Bluetooth Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit StormBox Blast 2 Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/turn-up-the-volume-with-these-tribit-bluetooth-speaker-deals-starting-at-usd32</link>
                                                                            <description>
                            <![CDATA[ Tribit offers great value, rugged speakers that don't compromise on quality, despite their small price tags. Take a look at these six feature-packed speakers and find the right one for you. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">dQgyN3fMSgMr874aPDhLCY</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/kAaWoTMtuaEHb4cmcQo6YJ-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 30 Jul 2026 13:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                                    <dc:creator><![CDATA[ Will Salmon ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/HpRWP7Mh9ErgRGoTvZBbBJ.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Will Salmon is Tom&#039;s Guide&#039;s Collections Editor, curating advice on the best products to spend your money on. Will has been writing about tech and entertainment for almost 20 years, including stints as GamesRadar+&#039;s Streaming Editor, editing comics website Newsarama, and writing for lots and lots of magazines.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/kAaWoTMtuaEHb4cmcQo6YJ-1280-80.jpg">
                                                            <media:credit><![CDATA[Tribit / Edited by Gemini]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A selection of Tribit Bluetooth speakers on a pale background.]]></media:description>                                                            <media:text><![CDATA[A selection of Tribit Bluetooth speakers on a pale background.]]></media:text>
                                <media:title type="plain"><![CDATA[A selection of Tribit Bluetooth speakers on a pale background.]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/kAaWoTMtuaEHb4cmcQo6YJ-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>There are lots of brands vying for your attention in the Bluetooth speaker space right now — so many, in fact, that it can be easy to ignore the lesser known ones in favor of the names you already know. But venturing beyond those obvious brands can reap some rewards, like discovering a range of great value speakers that offer dynamic, punchy audio and deep bass on a budget.</p><p>Such is the case with Tribit, which has become known for producing tough, compact Bluetooth speakers that often clock in at well under $50. These feature-packed speakers don't compromise on sound, despite their generally small size. </p><p>I've picked out six of my favorite Tribit speakers below, models that demonstrate what this great brand is capable of. What's more, four of these models are currently on sale, meaning you can get a great quality new speaker whatever your budget.</p>        <div class="featured_product_block featured_block_hero" data-id="a9a06412-8a8f-11f1-bd25-030df3d6064b">            <a href="https://www.amazon.com/dp/B07594HZ6Y?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Xsound Go Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:61.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/p8i4REtYUwQa3xLLPMDRBH.jpg" alt="Tribit Xsound Go Portable Bluetooth Speaker, 16w, 24h Playtime, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Xsound Go Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a06250-8a8f-11f1-a5d9-afa110733503">            <a href="https://www.amazon.com/dp/B0DSPCW5M5?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Stormbox Mini+ Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/C5nsJL7vL2zjn6R825kj9G.jpg" alt="Tribit Stormbox Mini+ Portable Bluetooth Speaker, 12w, 360° Sound, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Stormbox Mini+ Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a062be-8a8f-11f1-9929-a72ca031d0b8">            <a href="https://www.amazon.com/dp/B0GF6PPS4S?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Stormbox Micro 3 Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/KX6NK73tSzwTeeKkxpt8iG.jpg" alt="Tribit Stormbox Micro 3 Portable Bluetooth Speaker, 13w, Ip68 Waterproof"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Stormbox Micro 3 Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a061ce-8a8f-11f1-8d28-5741ec4d2c75">            <a href="https://www.amazon.com/dp/B0DN5F9BC2?lv=shuf&channelId=500&plpRedirect=mhFallback&th=1" data-model-name="Tribit StormBox Lava Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/JV6Q2uRVo9iY4M86HXumdM.jpg" alt="Tribit StormBox Lava Bluetooth Speaker."></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit StormBox Lava Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a0639a-8a8f-11f1-b9f8-2f0a56318b35">            <a href="https://www.amazon.com/dp/B09PMYTJD2?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit Stormbox Blast Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/KveW89RmovWCrw8QtASGpG.jpg" alt="Tribit Stormbox Blast Bluetooth Speaker, 90w Loud Sound, 30h Playtime"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit Stormbox Blast Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="a9a0632c-8a8f-11f1-9379-cfed3571fbf9">            <a href="https://www.amazon.com/dp/B0D9Y7N23T?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="Tribit StormBox Blast 2 Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/23hEjsPwDLdrapSHYjkkwV.jpg" alt="Tribit StormBox Blast 2 Bluetooth Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Tribit</div>                                        <div class="featured__title">Tribit StormBox Blast 2 Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just tested the brand-new Samsung QS90H — and it’s one of the best all-in-one soundbars money can buy ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Samsung QS09F is a beast of a soundbar. With a 7.1.2 channel configuration for under $999, this could be one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> if you need something compact yet powerful. There’s a built-in subwoofer and two side-firing surrounds for truly wide sound and immersive Dolby Atmos. </p><p>As per Samsung’s naming conventions, the QS90H is a part of 2026’s new soundbar lineup. Last year’s models had an “F” suffix. Unsure why Samsung skipped “G”, but if you see a soundbar with an “H” in the name, it’s new in 2026 (and old thereafter). </p><p>So what separates the Samsung QS90H from the rest of the new lineup? The QS90H can do it <em>all</em>. It’s one soundbar with a four-woofer built-in sub, surrounds, and thirteen speakers in the soundbar itself. This is an incredible performance for a one-bar device. Want to find out more? Keep reading this Samsung QS90H review.</p><h2 class="article-body__section" id="section-samsung-qs90h-review-specs"><span>Samsung QS90H review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.bestbuy.com/product/samsung-q-series-soundbar-hw-qs90h-za-7-1-2-ch-with-built-in-woofers-graphite-black/JJGRF3TGVQ" target="_blank" rel="nofollow">$999</a> / <a href="https://www.amazon.co.uk/Samsung-7-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMH3M2M" target="_blank" rel="nofollow">£899</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI eARC, optical, Bluetooth, Wi-Fi</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>14.8 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>2.7 x 49 x 4.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Built-in</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>7.1.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-samsung-qs90h-review-price-availability"><span>Samsung QS90H review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZeEfgNrDotQgnMsQTwJyfM" name="Samsung QS90H 01 4.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/ZeEfgNrDotQgnMsQTwJyfM.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Samsung QS90H is <a href="https://www.bestbuy.com/product/samsung-q-series-soundbar-hw-qs90h-za-7-1-2-ch-with-built-in-woofers-graphite-black/JJGRF3TGVQ" target="_blank" rel="nofollow">$999 from Best Buy</a> and <a href="https://www.amazon.co.uk/Samsung-7-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMH3M2M" target="_blank" rel="nofollow">£899 from Amazon U.K.</a> Although an objectively huge amount of money, you get a soundbar, a subwoofer, and surrounds for that $999 — <em>and</em> you don’t have to make extra space in your living room. </p><p>I’ve also tested the fantastic-value <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> ($899) and the gorgeous mid-range <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a> ($599). I’d recommend the Q600F if you’re not concerned with Wi-Fi music playback and the QS750F if you want separate sub and rears. However, these are 2025’s models now. I would be more than happy to use a 2025 models — the sound is still industry-leading as I write this in 2026 — but it’s just something to note if you’re the kind of person who always wants the newest tech. </p><p>I’ve also reviewed the fantastic <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300xmk2-review">JBL Bar 1300XMK2</a>. I would recommend this wholeheartedly if you want the best home cinema setup without needing to fork out on AVRs, floor standers, and the like.</p><h2 class="article-body__section" id="section-samsung-qs90h-review-design"><span>Samsung QS90H review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SZeiqFmnzsCnvoHTRaEqTN" name="Samsung QS90H 01 1.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/SZeiqFmnzsCnvoHTRaEqTN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The QS90H looks like any other soundbar. It won’t win any design awards, but it won’t steer attention away from your TV, either. It’s a pretty long 49 inches, so anything smaller than a 55” TV will look strange. I tested with the LG G6 OLED 55” and I’d say even that looked a bit weird. </p><p>The only frustrating design aspect is the HDMI port. Both the power port and HDMI port are angled, so you need a side-oriented cable. Of course, the soundbar comes with an angled power cable and HDMI, but if you want an aftermarket HDMI, you’ll need to find one that fits under the soundbar. </p><h2 id="controls-6">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PiR7TWTBek6f2aRGhkCrmN" name="Samsung QS90H 01 3.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/PiR7TWTBek6f2aRGhkCrmN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There are a few controls on the soundbar itself: volume up/down and power. There’s the classic Samsung remote with all the usual controls: input, Bluetooth, volume, woofer volume, sound mode, channel volume, and settings. I honestly did most of the controlling via the SmartThings virtual remote on my phone. </p><h2 id="features-4">Features</h2><p>I’ll go into app features in more detail below, but you’ve got all the expected features: sound modes, a few AI features, EQ adjustment, channel level adjustment, night mode, virtual mode, bass enhancement, and dialogue enhancement. </p><h2 class="article-body__section" id="section-samsung-qs90h-review-app"><span>Samsung QS90H review: App</span></h2><p>SmartThings is <em>by far</em> my favorite smart home app. It just makes sense. It’s easy to navigate, straightforward for newcomers, and has zero connectivity hiccups. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AHsh4VBmVyM4Ewmv8VK6Nn" name="samsung qs90h screenshots 1" alt="three screenshots from the samsung smart things app" src="https://cdn.mos.cms.futurecdn.net/AHsh4VBmVyM4Ewmv8VK6Nn.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Samsung / Tom's Guide)</span></figcaption></figure><p>As you can see, there’s a huge array of smart features: “SpaceFit Sound Pro”, “Active Voice Amplifier Pro”, “Sound Elevation”, and “Auto Volume”. Last year, “SpaceFit Sound Pro” only worked with an AI-powered Samsung TV, but I was able to turn it on with my LG G6 OLED. However, SpaceFit Sound Pro isn’t a traditional room calibration tool that sends out sound waves and analyzes them with built-in microphones, <em>à la</em> Klipsch or JBL. Honestly, I didn’t notice a huge difference between SpaceFit on and off, which is a shame. </p><p>You can also adjust each channel individually. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="msGF36KfjdfNU5i2mCTGdP" name="samsung app screenshotrs 2" alt="three screenshots from the samsung smartthings app" src="https://cdn.mos.cms.futurecdn.net/msGF36KfjdfNU5i2mCTGdP.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Samsung / Tom's Guide)</span></figcaption></figure><p>You can control the woofer volume, “side”, “center”, “wide”, and “front top”. I got the best sound with “side” and “wide” up to the max. I got a touch more height with “front top” set to max, but I still didn’t reach the huge highs I did with the JBL Bar 1300XMK2. </p><p>Finally, there’s wireless Dolby Atmos — but this <em>is</em> locked behind recent AI-enabled Samsung TVs. You can also sync up your Roon, Spotify, Tidal, and Alexa. </p><h3 class="article-body__section" id="section-samsung-qs90hs-review-sound-quality"><span>Samsung QS90HS review: Sound quality</span></h3><p>I test every soundbar with “Top Gun: Maverick” on 4K Blu-Ray, TV streaming, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-4">Movies</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nGwKfjPMyRvPj5aCsT3NkN" name="Samsung QS90H 01.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/nGwKfjPMyRvPj5aCsT3NkN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I watched “Top Gun: Maverick”, “The Super Mario Galaxy Movie”, and “Fight Club” on 4K Blu-ray. </p><p>The opening scene in “Top Gun: Maverick” is ideal for testing width, height, high-frequency sound, and bass simultaneously. I activated “Voice enhancement” and “Bass enhancement” in the SmartThings app, which brought all the high-frequency sounds to the center of the soundstage. Whooshing planes, whirring engines, flicking ropes, and tinkling metal diegetic sounds were focal and loud without pinching or overpowering the bass-heavy “Danger Zone”. I didn’t struggle to hear anything — this was a perfect performance. </p><p>Dialogue was clear, even in flight scenes with distortion from the radio. Engines were loud and clear, layered under the dialogue with zero overpowering. There was a <em>touch</em> of over-processing in some of Maverick’s radioed dialogue, but this was seriously negligible. It didn’t impact my viewing experience in the slightest. </p><p>There wasn’t as much height as I would like, which is a consistent issue with Samsung soundbars. The <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">QS750F</a> sounded expansive, but not massively high. If you want serious, room-filling height, you might want something like the JBL Bar 1300XMK2. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="KqAszSmfRCondDM9Ru7TkN" name="Samsung QS90H 01 2.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/KqAszSmfRCondDM9Ru7TkN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In the first dogfighting scene, the planes whooshed around the side of the soundscape, while the radioed dialogue and soundtrack were clear in the foreground. When the planes spiralled, I could feel it around me. However, I couldn’t hear the planes that were supposed ot be above me, which means the soundbar struggles with height. I think some more sophisticated room calibration might mitigate this in the future. </p><p>Next, I watched “The Super Mario Galaxy Movie”. The soundtrack in this movie was super expansive. Each violin motif sounded like it was coming from around me. The twinkling sound effects were particularly powerful. </p><p>It’s unlikely your kids would notice this, but the treble crackled when the spaceship took off in the opening scene. I didn’t notice any treble issues during the rest of the movie, though, or elsewhere in testing.</p><p>Lastly, I watched “Fight Club”. The sped-up sound effects squelched through the soundbar with fantastic width. Dialogue was clear and audible and the soundtrack sounded textured and controlled. Again — this was a perfect experience. No notes.</p><h2 id="tv-4">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="S3BDkGTgPKMKBDH7dHFQ9N" name="Samsung QS90H 01 5.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/S3BDkGTgPKMKBDH7dHFQ9N.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV performance, I watched “Heated Rivalry” on NOWTV, a British streaming service. I watched using “AI Adaptive Sound” in the SmartThings app, and upped the width and side channel volume to the max. </p><p>The soundbar heightened surround sound: in a locker room celebration scene, I could hear the spraying champagne and excited chatter in the background clearly, even over the exuberant soundtrack. There’s an aquarium scene later in the episode and the underwater-style diegetic sound effects married perfectly with the ethereal ambient OST. </p><h2 id="music-4">Music</h2><p>To test music playback, I listened to a number of songs on Qobuz. “Beatback” by Fcukers is an electronic track with a powerful bassline and groovy synth modulations in the verses and a breathy female vocal. The bass sounded particularly excellent, clear, rich, and propulsive, but the percussion sounded a <em>touch</em> underwater. Even so, the vocals were textured and sonorous. </p><p>Next, I played a psych-rock track, “Apocalypse Dreams” by Tame Impala. I switched off “Voice enhancement”, which massively improved the sound quality. It’s a shame there’s no dedicated music mode, but I recommend turning off both “Active Voice Amplifier Pro” and “Voice enhancement” to get the best from your music. The bass, again, was powerful and rich, but the treble was a touch pinchy. </p><p>Lastly, I played a guitar-led bluesy track: “Pigeon” by Faye Webster. This song has smooth guitars, nostalgic keys, and a soft female vocal. The vocals sounded particularly beautiful; super complex and warm. There’s some really nice stereo imaging after the first chorus that sounds immersive and all-consuming. The softer percussion didn’t pinch — the guitars were smooth throughout. This song sounded the best out of all the tracks I listened to, likely due to the sparser, warmer, smoother soundscape. </p><h2 class="article-body__section" id="section-samsung-qs90h-review-verdict"><span>Samsung QS90H review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9tzn4VbLc2oXygTDjgo8VN" name="Samsung QS90H 01_ 1.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/9tzn4VbLc2oXygTDjgo8VN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Samsung QS90H is the best all-in-one soundbar I’ve tested, and I’d go so far as to say it’s the best all-in-one on the market right now. I knocked off half a star for the sound height issues, but this isn’t to say the soundbar is lackluster, because that’s completely untrue. This is a powerful, controlled, and adept soundbar with a monumental soundstage. </p><p>If you want the “best” home cinema experience possible, you should probably look to spend a little more on something like the JBL Bar 1300XMK2 or the Samsung Q990H. However, if you’re short on space or don’t want to spend more than $1,000, then you will not be disappointed by the QS90H. </p><p>I loved testing the Samsung QS90H and would have zero reservations about buying this soundbar for myself. If you want a multi-channel experience without needing rears and a sub, there’s very little else I’d recommend. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/samsung-qs90h-review</link>
                                                                            <description>
                            <![CDATA[ The Samsung QS90H is a powerful all-in-one soundbar. No need for a separate subwoofer or rears here: 7.1.2 channel in just one device. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">eStcsbW422XXh5ZwWU7hLj</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/9tzn4VbLc2oXygTDjgo8VN-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sun, 26 Jul 2026 09:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/9tzn4VbLc2oXygTDjgo8VN-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom&#039;s guide background]]></media:description>                                                            <media:text><![CDATA[the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom&#039;s guide background]]></media:text>
                                <media:title type="plain"><![CDATA[the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom&#039;s guide background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/9tzn4VbLc2oXygTDjgo8VN-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Samsung QS09F is a beast of a soundbar. With a 7.1.2 channel configuration for under $999, this could be one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> if you need something compact yet powerful. There’s a built-in subwoofer and two side-firing surrounds for truly wide sound and immersive Dolby Atmos. </p><p>As per Samsung’s naming conventions, the QS90H is a part of 2026’s new soundbar lineup. Last year’s models had an “F” suffix. Unsure why Samsung skipped “G”, but if you see a soundbar with an “H” in the name, it’s new in 2026 (and old thereafter). </p><p>So what separates the Samsung QS90H from the rest of the new lineup? The QS90H can do it <em>all</em>. It’s one soundbar with a four-woofer built-in sub, surrounds, and thirteen speakers in the soundbar itself. This is an incredible performance for a one-bar device. Want to find out more? Keep reading this Samsung QS90H review.</p><h2 class="article-body__section" id="section-samsung-qs90h-review-specs"><span>Samsung QS90H review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.bestbuy.com/product/samsung-q-series-soundbar-hw-qs90h-za-7-1-2-ch-with-built-in-woofers-graphite-black/JJGRF3TGVQ" target="_blank" rel="nofollow">$999</a> / <a href="https://www.amazon.co.uk/Samsung-7-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMH3M2M" target="_blank" rel="nofollow">£899</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI eARC, optical, Bluetooth, Wi-Fi</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>14.8 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>2.7 x 49 x 4.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Built-in</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>7.1.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-samsung-qs90h-review-price-availability"><span>Samsung QS90H review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZeEfgNrDotQgnMsQTwJyfM" name="Samsung QS90H 01 4.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/ZeEfgNrDotQgnMsQTwJyfM.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Samsung QS90H is <a href="https://www.bestbuy.com/product/samsung-q-series-soundbar-hw-qs90h-za-7-1-2-ch-with-built-in-woofers-graphite-black/JJGRF3TGVQ" target="_blank" rel="nofollow">$999 from Best Buy</a> and <a href="https://www.amazon.co.uk/Samsung-7-1-2ch-Soundbar-Subwoofer-AirPlay/dp/B0GTMH3M2M" target="_blank" rel="nofollow">£899 from Amazon U.K.</a> Although an objectively huge amount of money, you get a soundbar, a subwoofer, and surrounds for that $999 — <em>and</em> you don’t have to make extra space in your living room. </p><p>I’ve also tested the fantastic-value <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> ($899) and the gorgeous mid-range <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a> ($599). I’d recommend the Q600F if you’re not concerned with Wi-Fi music playback and the QS750F if you want separate sub and rears. However, these are 2025’s models now. I would be more than happy to use a 2025 models — the sound is still industry-leading as I write this in 2026 — but it’s just something to note if you’re the kind of person who always wants the newest tech. </p><p>I’ve also reviewed the fantastic <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300xmk2-review">JBL Bar 1300XMK2</a>. I would recommend this wholeheartedly if you want the best home cinema setup without needing to fork out on AVRs, floor standers, and the like.</p><h2 class="article-body__section" id="section-samsung-qs90h-review-design"><span>Samsung QS90H review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SZeiqFmnzsCnvoHTRaEqTN" name="Samsung QS90H 01 1.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/SZeiqFmnzsCnvoHTRaEqTN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The QS90H looks like any other soundbar. It won’t win any design awards, but it won’t steer attention away from your TV, either. It’s a pretty long 49 inches, so anything smaller than a 55” TV will look strange. I tested with the LG G6 OLED 55” and I’d say even that looked a bit weird. </p><p>The only frustrating design aspect is the HDMI port. Both the power port and HDMI port are angled, so you need a side-oriented cable. Of course, the soundbar comes with an angled power cable and HDMI, but if you want an aftermarket HDMI, you’ll need to find one that fits under the soundbar. </p><h2 id="controls-6">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PiR7TWTBek6f2aRGhkCrmN" name="Samsung QS90H 01 3.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/PiR7TWTBek6f2aRGhkCrmN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There are a few controls on the soundbar itself: volume up/down and power. There’s the classic Samsung remote with all the usual controls: input, Bluetooth, volume, woofer volume, sound mode, channel volume, and settings. I honestly did most of the controlling via the SmartThings virtual remote on my phone. </p><h2 id="features-4">Features</h2><p>I’ll go into app features in more detail below, but you’ve got all the expected features: sound modes, a few AI features, EQ adjustment, channel level adjustment, night mode, virtual mode, bass enhancement, and dialogue enhancement. </p><h2 class="article-body__section" id="section-samsung-qs90h-review-app"><span>Samsung QS90H review: App</span></h2><p>SmartThings is <em>by far</em> my favorite smart home app. It just makes sense. It’s easy to navigate, straightforward for newcomers, and has zero connectivity hiccups. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AHsh4VBmVyM4Ewmv8VK6Nn" name="samsung qs90h screenshots 1" alt="three screenshots from the samsung smart things app" src="https://cdn.mos.cms.futurecdn.net/AHsh4VBmVyM4Ewmv8VK6Nn.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Samsung / Tom's Guide)</span></figcaption></figure><p>As you can see, there’s a huge array of smart features: “SpaceFit Sound Pro”, “Active Voice Amplifier Pro”, “Sound Elevation”, and “Auto Volume”. Last year, “SpaceFit Sound Pro” only worked with an AI-powered Samsung TV, but I was able to turn it on with my LG G6 OLED. However, SpaceFit Sound Pro isn’t a traditional room calibration tool that sends out sound waves and analyzes them with built-in microphones, <em>à la</em> Klipsch or JBL. Honestly, I didn’t notice a huge difference between SpaceFit on and off, which is a shame. </p><p>You can also adjust each channel individually. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="msGF36KfjdfNU5i2mCTGdP" name="samsung app screenshotrs 2" alt="three screenshots from the samsung smartthings app" src="https://cdn.mos.cms.futurecdn.net/msGF36KfjdfNU5i2mCTGdP.jpg" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Samsung / Tom's Guide)</span></figcaption></figure><p>You can control the woofer volume, “side”, “center”, “wide”, and “front top”. I got the best sound with “side” and “wide” up to the max. I got a touch more height with “front top” set to max, but I still didn’t reach the huge highs I did with the JBL Bar 1300XMK2. </p><p>Finally, there’s wireless Dolby Atmos — but this <em>is</em> locked behind recent AI-enabled Samsung TVs. You can also sync up your Roon, Spotify, Tidal, and Alexa. </p><h3 class="article-body__section" id="section-samsung-qs90hs-review-sound-quality"><span>Samsung QS90HS review: Sound quality</span></h3><p>I test every soundbar with “Top Gun: Maverick” on 4K Blu-Ray, TV streaming, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-4">Movies</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nGwKfjPMyRvPj5aCsT3NkN" name="Samsung QS90H 01.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/nGwKfjPMyRvPj5aCsT3NkN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I watched “Top Gun: Maverick”, “The Super Mario Galaxy Movie”, and “Fight Club” on 4K Blu-ray. </p><p>The opening scene in “Top Gun: Maverick” is ideal for testing width, height, high-frequency sound, and bass simultaneously. I activated “Voice enhancement” and “Bass enhancement” in the SmartThings app, which brought all the high-frequency sounds to the center of the soundstage. Whooshing planes, whirring engines, flicking ropes, and tinkling metal diegetic sounds were focal and loud without pinching or overpowering the bass-heavy “Danger Zone”. I didn’t struggle to hear anything — this was a perfect performance. </p><p>Dialogue was clear, even in flight scenes with distortion from the radio. Engines were loud and clear, layered under the dialogue with zero overpowering. There was a <em>touch</em> of over-processing in some of Maverick’s radioed dialogue, but this was seriously negligible. It didn’t impact my viewing experience in the slightest. </p><p>There wasn’t as much height as I would like, which is a consistent issue with Samsung soundbars. The <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">QS750F</a> sounded expansive, but not massively high. If you want serious, room-filling height, you might want something like the JBL Bar 1300XMK2. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="KqAszSmfRCondDM9Ru7TkN" name="Samsung QS90H 01 2.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/KqAszSmfRCondDM9Ru7TkN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In the first dogfighting scene, the planes whooshed around the side of the soundscape, while the radioed dialogue and soundtrack were clear in the foreground. When the planes spiralled, I could feel it around me. However, I couldn’t hear the planes that were supposed ot be above me, which means the soundbar struggles with height. I think some more sophisticated room calibration might mitigate this in the future. </p><p>Next, I watched “The Super Mario Galaxy Movie”. The soundtrack in this movie was super expansive. Each violin motif sounded like it was coming from around me. The twinkling sound effects were particularly powerful. </p><p>It’s unlikely your kids would notice this, but the treble crackled when the spaceship took off in the opening scene. I didn’t notice any treble issues during the rest of the movie, though, or elsewhere in testing.</p><p>Lastly, I watched “Fight Club”. The sped-up sound effects squelched through the soundbar with fantastic width. Dialogue was clear and audible and the soundtrack sounded textured and controlled. Again — this was a perfect experience. No notes.</p><h2 id="tv-4">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="S3BDkGTgPKMKBDH7dHFQ9N" name="Samsung QS90H 01 5.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/S3BDkGTgPKMKBDH7dHFQ9N.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test TV performance, I watched “Heated Rivalry” on NOWTV, a British streaming service. I watched using “AI Adaptive Sound” in the SmartThings app, and upped the width and side channel volume to the max. </p><p>The soundbar heightened surround sound: in a locker room celebration scene, I could hear the spraying champagne and excited chatter in the background clearly, even over the exuberant soundtrack. There’s an aquarium scene later in the episode and the underwater-style diegetic sound effects married perfectly with the ethereal ambient OST. </p><h2 id="music-4">Music</h2><p>To test music playback, I listened to a number of songs on Qobuz. “Beatback” by Fcukers is an electronic track with a powerful bassline and groovy synth modulations in the verses and a breathy female vocal. The bass sounded particularly excellent, clear, rich, and propulsive, but the percussion sounded a <em>touch</em> underwater. Even so, the vocals were textured and sonorous. </p><p>Next, I played a psych-rock track, “Apocalypse Dreams” by Tame Impala. I switched off “Voice enhancement”, which massively improved the sound quality. It’s a shame there’s no dedicated music mode, but I recommend turning off both “Active Voice Amplifier Pro” and “Voice enhancement” to get the best from your music. The bass, again, was powerful and rich, but the treble was a touch pinchy. </p><p>Lastly, I played a guitar-led bluesy track: “Pigeon” by Faye Webster. This song has smooth guitars, nostalgic keys, and a soft female vocal. The vocals sounded particularly beautiful; super complex and warm. There’s some really nice stereo imaging after the first chorus that sounds immersive and all-consuming. The softer percussion didn’t pinch — the guitars were smooth throughout. This song sounded the best out of all the tracks I listened to, likely due to the sparser, warmer, smoother soundscape. </p><h2 class="article-body__section" id="section-samsung-qs90h-review-verdict"><span>Samsung QS90H review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9tzn4VbLc2oXygTDjgo8VN" name="Samsung QS90H 01_ 1.JPG" alt="the samsung qs90h soundbar in a living room set with a tv and photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/9tzn4VbLc2oXygTDjgo8VN.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Samsung QS90H is the best all-in-one soundbar I’ve tested, and I’d go so far as to say it’s the best all-in-one on the market right now. I knocked off half a star for the sound height issues, but this isn’t to say the soundbar is lackluster, because that’s completely untrue. This is a powerful, controlled, and adept soundbar with a monumental soundstage. </p><p>If you want the “best” home cinema experience possible, you should probably look to spend a little more on something like the JBL Bar 1300XMK2 or the Samsung Q990H. However, if you’re short on space or don’t want to spend more than $1,000, then you will not be disappointed by the QS90H. </p><p>I loved testing the Samsung QS90H and would have zero reservations about buying this soundbar for myself. If you want a multi-channel experience without needing rears and a sub, there’s very little else I’d recommend. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tested the Denon Home 600 — and it’s one of the most ambitious smart speakers yet ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Denon Home 600 has all the makings of one of the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a>. Sound is the most important thing, right? You’ll be pleased to know that the Home 600 sounds fantastic. There’s enviable balance across the entire frequency range, rich bass, and energetic mids. </p><p>So why only a 3.5-star rating? Why not 5? Two reasons: one, the price. $799 is a <em>lot</em> to spend on a home speaker that doesn’t even have its own built-in smart assistant. Two, the companion app: HEOS works, but it has major issues. While not as speaker-breaking as Sonos’ headline-making app problems last year, HEOS just feels like it was designed to be frustrating. And when you’re spending $800 on a speaker, there’s simply no room for these rookie errors. </p><p>But if you want a seriously good home speaker that will have your guests going “Ooh” and “Aah”, then you’ll definitely want to learn more. To find out, keep reading this Denon Home 600 review. </p><h2 class="article-body__section" id="section-denon-home-600-review-specs"><span>Denon Home 600 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Denon-Wireless-8-Driver-Multiroom-Bluetooth/dp/B0GLSQK5W2" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/Denon-Bluetooth-Multiroom-Subwoofers-High-Resolution/dp/B0GN3W8RSZ" target="_blank" rel="nofollow">£599</a></p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Charcoal, stone</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>9.9 x 17.7 x 8.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>17.6 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>5.1</p></td></tr><tr><td class="firstcol " ><p><strong>Smart assistance</strong></p></td><td  ><p>Siri (with HomePod)</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wi-Fi 2.4GHz, 5GHz, AirPlay 2, Bluetooth, aux, USB-C (for ethernet adaptor) </p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-denon-home-600-review-price-availability"><span>Denon Home 600 review: Price & availability </span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HHSSoESrj5cbHfAtsdsYo8" name="Denon Home 600_ 2.JPG" alt="the denon home 600 home speaker in stone color photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/HHSSoESrj5cbHfAtsdsYo8.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s no way around it: the Denon Home 600 is very, very, <em>very</em> expensive. It’s a whopping <a href="https://www.amazon.com/Denon-Wireless-8-Driver-Multiroom-Bluetooth/dp/B0GLSQK5W2" target="_blank" rel="nofollow">$799 from Amazon U.S.</a> and <a href="https://www.amazon.co.uk/Denon-Bluetooth-Multiroom-Subwoofers-High-Resolution/dp/B0GN3W8RSZ" target="_blank" rel="nofollow">£599 from Amazon U.K.</a> This is considerably pricier than any competition — the <a href="https://www.tomsguide.com/audio/smart-speakers/bose-lifestyle-ultra-speaker-review">Bose Lifestyle Ultra</a> ($299, although more comparable to Denon’s $399 Home 200 in size), the <a href="https://www.tomsguide.com/reviews/apple-homepod-2nd-generation">Apple HomePod 2</a> ($299), <a href="https://www.amazon.com/Google-Audio-Bluetooth-Speaker-Built/dp/B0FY3YXCT1" target="_blank" rel="nofollow">Google Audio Home Speaker </a>($119), and even its smaller sibling, the <a href="https://www.tomsguide.com/audio/speakers/denon-home-400-review">Denon Home 400</a> ($599). Even <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos’s Era 300 </a>is $499 — yes, $300 cheaper than the Denon Home 600. </p><p>There’s not really anything in the Home 600’s ballpark. It’s bigger, heavier, and more expensive than any other home speaker I’ve come across. This is a <em>speaker</em>. No, this is a Speaker. Capital letters required here. </p><h2 class="article-body__section" id="section-denon-home-600-review-design"><span>Denon Home 600 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="L8hw6k7buSet8bUQAe6bWG" name="Denon Home 600 16.JPG" alt="the denon home 600 home speaker photographed with the blue tom's guid e background" src="https://cdn.mos.cms.futurecdn.net/L8hw6k7buSet8bUQAe6bWG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Just like the Denon Home 400, the Home 600 is beautiful. It’s <em>huge</em>, at almost 18 inches wide and 9 inches deep. Obviously, I don’t know the layout of your home, but this speaker didn’t fit anywhere in my apartment. The Home 400 looks beautiful on my mantlepiece, but the 600 was too deep to fit here. I had to put it on the coffee table. For this reason, I wouldn’t recommend it for small homes. </p><p>If you’ve got the space, though, it’ll look gorgeous in your place. The speaker comes in charcoal or stone — obviously, I tested stone. The mesh cover is pleasing to the eye, and the buttons are tactile and accessible. I especially like the brushed metal casing and the tactile plastic on the front speaker; features like this just make it feel more elevated and elegant than Bose, Sonos, and Apple’s equivalents. </p><p>Another thing that makes the Home 600 feel elevated? The 5.1 speaker configuration. Yes, the Home 600 has five speakers and a built-in subwoofer, all with individual pre-amps. In real terms, this means the speakers aren’t fighting for power, and the speaker itself has an impressive 170W of audio power. </p><h2 id="connectivity-3">Connectivity</h2><p>Apart from the gargantuan size, what sets the Denon Home 600 apart from other smart speakers is the range of connectivity options. Rather than just being a Wi-Fi or Bluetooth speaker — which it can do — it also has hi-res USB-C playback and a 3.5mm input. </p><p>As this is a home speaker, I listened via the HEOS app and Spotify Connect.</p><h2 id="smart-home-features">Smart home features</h2><p>As with the Denon Home 400, the Home 600 isn’t actually a “smart speaker” in the traditional sense… in that it can’t actually function as a smart speaker. If you have a HomePod, you can add the Denon Home collection to your Apple Home, but the speakers don’t have a native smart assistant. Consequently, you cannot use them as you might expect — no “Alexa, set a timer for 10 minutes” or “Hey Google, put a note in my calendar”. The speakers simply do not function like this. </p><p>As I don’t have a HomePod, the Denon Home 600 was effectively just a home speaker with AirPlay and Bluetooth (and USB-C playback). Don’t get me wrong, it’s a great speaker, but this is a pretty major con if you’re looking specifically for a <em>smart</em> speaker. You’d be better off with the Google Nest, Bose, a Sonos, or Apple HomePod 2.</p><h2 class="article-body__section" id="section-denon-home-600-review-sound-quality"><span>Denon Home 600 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jeNxUHUowCdYN5runNurP8" name="Denon Home 600_ 3.JPG" alt="the denon home 600 home speaker in stone color photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/jeNxUHUowCdYN5runNurP8.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As with the Denon Home 400, the 600 sounds great. It has a range of connectivity options, as discussed above, which will impress audiophiles more than, say, the Bose Lifestyle Speaker. Due to its positioning as a smart/wireless speaker, I tested via <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service for audiophiles</a>, built into HEOS. </p><p>Before listening, the HEOS app will prompt you to calibrate your sound based on the room. You don’t have to do anything other than tell the app how close the speaker is to a wall.</p><p>First, I played <em>Call Me</em> by Blondie. The ravenous guitars were powerful and rich through the Home 600, and the vocals were clear and expansive. Every layer of the track was nicely balanced, and I could even hear the synths skating across the guitars. Blondie sounded great here.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="E6bqJJKDY7M2iR22Pt5aUG" name="Denon Home 600 17.JPG" alt="the denon home 600 home speaker photographed with the blue tom's guid e background" src="https://cdn.mos.cms.futurecdn.net/E6bqJJKDY7M2iR22Pt5aUG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p><em>Jesus of Suburbia</em> by Green Day is a little heavier, with louder guitars, more overwhelming bass, and more raucous percussion. The Home 600 did struggle a little separating the vocals here — Billy Joe Armstrong’s own guitar drowned him out, so I had to turn down the bass. This helped poor Billy Joe to be heard over the instrumental, but the vocals were still a little quiet. </p><p>I listened to a few more rock songs from a similar era — Foo Fighters, Nirvana, and Muse — and it was just the Green Day track that suffered from suppressed vocals. </p><p>Last, I played <em>Tambourine</em> by The Dare, an electronic track with wobbly synth notes and processed vocals. This song sounded fantastic, with powerful, punchy bass and trickling synth notes clear in the top end of the track. It was super wide, clean, and overall just a fantastic performance. </p><p>The Denon Home 600 sounds fantastic. It has a wide, high soundscape with delicate handling of soft, high-frequency sounds. The low-end is controlled, but can be insistent and powerful with the bass upped to max. Although I wish the treble was a little less sharp in some genres — rock particularly — this didn’t upset me that much. </p><p>Oh, and when I grouped the Home 400 and 600? Magic. I just wish the speaker wasn’t held back by HEOS usability issues… which I’ll get into now. </p><h2 class="article-body__section" id="section-denon-home-600-review-app"><span>Denon Home 600 review: App</span></h2><p>If you’re familiar with Denon/Marantz products, you’ll likely also be familiar with HEOS. I’m in a love/hate relationship with HEOS. On the one hand, its new design is visually appealing.</p><p>On the other hand, it can be just ever-so-slightly frustrating. As I now have the Denon Home 400 and Denon Home 600, I can group my speakers to play the same music or ungroup them to play different songs. Well, kind of. You can group the speakers easily, but if you want to switch the master speaker, you can’t. You have to manually play the same playlist from the secondary speaker and manually turn off the master speaker, and <em>then</em> regroup. Yep, annoying. </p><p>However, if you want to stream through both speakers via Spotify, you have to do this from Spotify itself rather than HEOS. Confused? Yeah, me too. Here’s a screenshot so you can see what I mean.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8j2XzbCbonWoSdi6Hi7Kvn" name="denon heos screenshots" alt="three screenshots from the HEOS app" src="https://cdn.mos.cms.futurecdn.net/8j2XzbCbonWoSdi6Hi7Kvn.png" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Denon / Future)</span></figcaption></figure><p>HEOS is a quirky app. For example, you need to go into the ‘Rooms’ tab and activate your desired speaker before searching for music; otherwise, it will play using the last active speaker. You can’t click on the name at the bottom and select from your connected speakers. Again — confused? Me, too. </p><p>You also can’t use the built-in streaming services as normal in HEOS. For example, you can’t click on an artist name to view the artist page. You can either choose from Qobuz’s playlists or search for your own. If you want to peruse your own playlists, you have to scroll all the way to the bottom of the Qobuz widget. On top of this, after a song finishes, HEOS doesn’t play the rest of the playlist. It autoplays from previously played tracks. You have to click ‘Play all’ or ‘Shuffle’. It’s just little things like this that make HEOS a touch non-user-friendly.</p><p>So while HEOS is quirky, it just depends on whether you have the will and patience to learn. At $800, though, a companion app shouldn’t be quirky. It should just work. But this unfortunately seems par for the course. Remember the Sonos app drama last year? What is it with audio brands releasing fantastic speakers with unfortunate app issues? </p><h2 class="article-body__section" id="section-denon-home-600-review-verdict"><span>Denon Home 600 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="MBkKUJc8xSLPYMebNiBEWG" name="Denon Home 600 18.JPG" alt="the denon home 600 home speaker photographed with the blue tom's guid e background" src="https://cdn.mos.cms.futurecdn.net/MBkKUJc8xSLPYMebNiBEWG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Denon Home 600 is an incredible speaker. Sorry, Speaker (if you remember the lame joke I made at the start of this review). It sounds gorgeous, with lively bass and subtle detail in the high-end. With two Denon Homes, the grouped room playback is awesome. </p><p>However, this speaker has a 3.5-star rating for a reason, and that reason is HEOS-shaped. I want to give this speaker 4 stars, because its sound and design deserve it. But HEOS is a relentless beast intent on making me want to pull my hair out. There are quality-of-life issues that require fixing before I can wholeheartedly recommend the Denon Home 600. The Home 400 was cheaper, so these issues were easier to overlook. But $800? Nah. It’s gotta be perfect if you’re asking for almost a grand. </p><p>If you can look past app issues, then you won’t be disappointed by the Home 600. It sounds beautiful, looks beautiful, and comes so close. Just ignore HEOS, play direct from your streaming app, and it could be a 4.5-star product.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/denon-home-600-review</link>
                                                                            <description>
                            <![CDATA[ The Denon Home 600 smart speaker is huge at 18 inches long, with 8 speakers and a built-in sub. But it’s pricey and has a few app-related issues. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">ofQXn6Um7PZMPHJjXeZSKP</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/VoRMrQif6TNTxeo3j7Pko8-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 20 Jul 2026 13:31:10 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/VoRMrQif6TNTxeo3j7Pko8-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the denon home 600 home speaker in stone color photographed against the blue tom&#039;s guide background]]></media:description>                                                            <media:text><![CDATA[the denon home 600 home speaker in stone color photographed against the blue tom&#039;s guide background]]></media:text>
                                <media:title type="plain"><![CDATA[the denon home 600 home speaker in stone color photographed against the blue tom&#039;s guide background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/VoRMrQif6TNTxeo3j7Pko8-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Denon Home 600 has all the makings of one of the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a>. Sound is the most important thing, right? You’ll be pleased to know that the Home 600 sounds fantastic. There’s enviable balance across the entire frequency range, rich bass, and energetic mids. </p><p>So why only a 3.5-star rating? Why not 5? Two reasons: one, the price. $799 is a <em>lot</em> to spend on a home speaker that doesn’t even have its own built-in smart assistant. Two, the companion app: HEOS works, but it has major issues. While not as speaker-breaking as Sonos’ headline-making app problems last year, HEOS just feels like it was designed to be frustrating. And when you’re spending $800 on a speaker, there’s simply no room for these rookie errors. </p><p>But if you want a seriously good home speaker that will have your guests going “Ooh” and “Aah”, then you’ll definitely want to learn more. To find out, keep reading this Denon Home 600 review. </p><h2 class="article-body__section" id="section-denon-home-600-review-specs"><span>Denon Home 600 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Denon-Wireless-8-Driver-Multiroom-Bluetooth/dp/B0GLSQK5W2" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/Denon-Bluetooth-Multiroom-Subwoofers-High-Resolution/dp/B0GN3W8RSZ" target="_blank" rel="nofollow">£599</a></p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Charcoal, stone</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>9.9 x 17.7 x 8.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>17.6 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>5.1</p></td></tr><tr><td class="firstcol " ><p><strong>Smart assistance</strong></p></td><td  ><p>Siri (with HomePod)</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wi-Fi 2.4GHz, 5GHz, AirPlay 2, Bluetooth, aux, USB-C (for ethernet adaptor) </p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-denon-home-600-review-price-availability"><span>Denon Home 600 review: Price & availability </span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HHSSoESrj5cbHfAtsdsYo8" name="Denon Home 600_ 2.JPG" alt="the denon home 600 home speaker in stone color photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/HHSSoESrj5cbHfAtsdsYo8.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s no way around it: the Denon Home 600 is very, very, <em>very</em> expensive. It’s a whopping <a href="https://www.amazon.com/Denon-Wireless-8-Driver-Multiroom-Bluetooth/dp/B0GLSQK5W2" target="_blank" rel="nofollow">$799 from Amazon U.S.</a> and <a href="https://www.amazon.co.uk/Denon-Bluetooth-Multiroom-Subwoofers-High-Resolution/dp/B0GN3W8RSZ" target="_blank" rel="nofollow">£599 from Amazon U.K.</a> This is considerably pricier than any competition — the <a href="https://www.tomsguide.com/audio/smart-speakers/bose-lifestyle-ultra-speaker-review">Bose Lifestyle Ultra</a> ($299, although more comparable to Denon’s $399 Home 200 in size), the <a href="https://www.tomsguide.com/reviews/apple-homepod-2nd-generation">Apple HomePod 2</a> ($299), <a href="https://www.amazon.com/Google-Audio-Bluetooth-Speaker-Built/dp/B0FY3YXCT1" target="_blank" rel="nofollow">Google Audio Home Speaker </a>($119), and even its smaller sibling, the <a href="https://www.tomsguide.com/audio/speakers/denon-home-400-review">Denon Home 400</a> ($599). Even <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos’s Era 300 </a>is $499 — yes, $300 cheaper than the Denon Home 600. </p><p>There’s not really anything in the Home 600’s ballpark. It’s bigger, heavier, and more expensive than any other home speaker I’ve come across. This is a <em>speaker</em>. No, this is a Speaker. Capital letters required here. </p><h2 class="article-body__section" id="section-denon-home-600-review-design"><span>Denon Home 600 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="L8hw6k7buSet8bUQAe6bWG" name="Denon Home 600 16.JPG" alt="the denon home 600 home speaker photographed with the blue tom's guid e background" src="https://cdn.mos.cms.futurecdn.net/L8hw6k7buSet8bUQAe6bWG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Just like the Denon Home 400, the Home 600 is beautiful. It’s <em>huge</em>, at almost 18 inches wide and 9 inches deep. Obviously, I don’t know the layout of your home, but this speaker didn’t fit anywhere in my apartment. The Home 400 looks beautiful on my mantlepiece, but the 600 was too deep to fit here. I had to put it on the coffee table. For this reason, I wouldn’t recommend it for small homes. </p><p>If you’ve got the space, though, it’ll look gorgeous in your place. The speaker comes in charcoal or stone — obviously, I tested stone. The mesh cover is pleasing to the eye, and the buttons are tactile and accessible. I especially like the brushed metal casing and the tactile plastic on the front speaker; features like this just make it feel more elevated and elegant than Bose, Sonos, and Apple’s equivalents. </p><p>Another thing that makes the Home 600 feel elevated? The 5.1 speaker configuration. Yes, the Home 600 has five speakers and a built-in subwoofer, all with individual pre-amps. In real terms, this means the speakers aren’t fighting for power, and the speaker itself has an impressive 170W of audio power. </p><h2 id="connectivity-3">Connectivity</h2><p>Apart from the gargantuan size, what sets the Denon Home 600 apart from other smart speakers is the range of connectivity options. Rather than just being a Wi-Fi or Bluetooth speaker — which it can do — it also has hi-res USB-C playback and a 3.5mm input. </p><p>As this is a home speaker, I listened via the HEOS app and Spotify Connect.</p><h2 id="smart-home-features">Smart home features</h2><p>As with the Denon Home 400, the Home 600 isn’t actually a “smart speaker” in the traditional sense… in that it can’t actually function as a smart speaker. If you have a HomePod, you can add the Denon Home collection to your Apple Home, but the speakers don’t have a native smart assistant. Consequently, you cannot use them as you might expect — no “Alexa, set a timer for 10 minutes” or “Hey Google, put a note in my calendar”. The speakers simply do not function like this. </p><p>As I don’t have a HomePod, the Denon Home 600 was effectively just a home speaker with AirPlay and Bluetooth (and USB-C playback). Don’t get me wrong, it’s a great speaker, but this is a pretty major con if you’re looking specifically for a <em>smart</em> speaker. You’d be better off with the Google Nest, Bose, a Sonos, or Apple HomePod 2.</p><h2 class="article-body__section" id="section-denon-home-600-review-sound-quality"><span>Denon Home 600 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jeNxUHUowCdYN5runNurP8" name="Denon Home 600_ 3.JPG" alt="the denon home 600 home speaker in stone color photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/jeNxUHUowCdYN5runNurP8.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As with the Denon Home 400, the 600 sounds great. It has a range of connectivity options, as discussed above, which will impress audiophiles more than, say, the Bose Lifestyle Speaker. Due to its positioning as a smart/wireless speaker, I tested via <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service for audiophiles</a>, built into HEOS. </p><p>Before listening, the HEOS app will prompt you to calibrate your sound based on the room. You don’t have to do anything other than tell the app how close the speaker is to a wall.</p><p>First, I played <em>Call Me</em> by Blondie. The ravenous guitars were powerful and rich through the Home 600, and the vocals were clear and expansive. Every layer of the track was nicely balanced, and I could even hear the synths skating across the guitars. Blondie sounded great here.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="E6bqJJKDY7M2iR22Pt5aUG" name="Denon Home 600 17.JPG" alt="the denon home 600 home speaker photographed with the blue tom's guid e background" src="https://cdn.mos.cms.futurecdn.net/E6bqJJKDY7M2iR22Pt5aUG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p><em>Jesus of Suburbia</em> by Green Day is a little heavier, with louder guitars, more overwhelming bass, and more raucous percussion. The Home 600 did struggle a little separating the vocals here — Billy Joe Armstrong’s own guitar drowned him out, so I had to turn down the bass. This helped poor Billy Joe to be heard over the instrumental, but the vocals were still a little quiet. </p><p>I listened to a few more rock songs from a similar era — Foo Fighters, Nirvana, and Muse — and it was just the Green Day track that suffered from suppressed vocals. </p><p>Last, I played <em>Tambourine</em> by The Dare, an electronic track with wobbly synth notes and processed vocals. This song sounded fantastic, with powerful, punchy bass and trickling synth notes clear in the top end of the track. It was super wide, clean, and overall just a fantastic performance. </p><p>The Denon Home 600 sounds fantastic. It has a wide, high soundscape with delicate handling of soft, high-frequency sounds. The low-end is controlled, but can be insistent and powerful with the bass upped to max. Although I wish the treble was a little less sharp in some genres — rock particularly — this didn’t upset me that much. </p><p>Oh, and when I grouped the Home 400 and 600? Magic. I just wish the speaker wasn’t held back by HEOS usability issues… which I’ll get into now. </p><h2 class="article-body__section" id="section-denon-home-600-review-app"><span>Denon Home 600 review: App</span></h2><p>If you’re familiar with Denon/Marantz products, you’ll likely also be familiar with HEOS. I’m in a love/hate relationship with HEOS. On the one hand, its new design is visually appealing.</p><p>On the other hand, it can be just ever-so-slightly frustrating. As I now have the Denon Home 400 and Denon Home 600, I can group my speakers to play the same music or ungroup them to play different songs. Well, kind of. You can group the speakers easily, but if you want to switch the master speaker, you can’t. You have to manually play the same playlist from the secondary speaker and manually turn off the master speaker, and <em>then</em> regroup. Yep, annoying. </p><p>However, if you want to stream through both speakers via Spotify, you have to do this from Spotify itself rather than HEOS. Confused? Yeah, me too. Here’s a screenshot so you can see what I mean.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8j2XzbCbonWoSdi6Hi7Kvn" name="denon heos screenshots" alt="three screenshots from the HEOS app" src="https://cdn.mos.cms.futurecdn.net/8j2XzbCbonWoSdi6Hi7Kvn.png" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Denon / Future)</span></figcaption></figure><p>HEOS is a quirky app. For example, you need to go into the ‘Rooms’ tab and activate your desired speaker before searching for music; otherwise, it will play using the last active speaker. You can’t click on the name at the bottom and select from your connected speakers. Again — confused? Me, too. </p><p>You also can’t use the built-in streaming services as normal in HEOS. For example, you can’t click on an artist name to view the artist page. You can either choose from Qobuz’s playlists or search for your own. If you want to peruse your own playlists, you have to scroll all the way to the bottom of the Qobuz widget. On top of this, after a song finishes, HEOS doesn’t play the rest of the playlist. It autoplays from previously played tracks. You have to click ‘Play all’ or ‘Shuffle’. It’s just little things like this that make HEOS a touch non-user-friendly.</p><p>So while HEOS is quirky, it just depends on whether you have the will and patience to learn. At $800, though, a companion app shouldn’t be quirky. It should just work. But this unfortunately seems par for the course. Remember the Sonos app drama last year? What is it with audio brands releasing fantastic speakers with unfortunate app issues? </p><h2 class="article-body__section" id="section-denon-home-600-review-verdict"><span>Denon Home 600 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="MBkKUJc8xSLPYMebNiBEWG" name="Denon Home 600 18.JPG" alt="the denon home 600 home speaker photographed with the blue tom's guid e background" src="https://cdn.mos.cms.futurecdn.net/MBkKUJc8xSLPYMebNiBEWG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Denon Home 600 is an incredible speaker. Sorry, Speaker (if you remember the lame joke I made at the start of this review). It sounds gorgeous, with lively bass and subtle detail in the high-end. With two Denon Homes, the grouped room playback is awesome. </p><p>However, this speaker has a 3.5-star rating for a reason, and that reason is HEOS-shaped. I want to give this speaker 4 stars, because its sound and design deserve it. But HEOS is a relentless beast intent on making me want to pull my hair out. There are quality-of-life issues that require fixing before I can wholeheartedly recommend the Denon Home 600. The Home 400 was cheaper, so these issues were easier to overlook. But $800? Nah. It’s gotta be perfect if you’re asking for almost a grand. </p><p>If you can look past app issues, then you won’t be disappointed by the Home 600. It sounds beautiful, looks beautiful, and comes so close. Just ignore HEOS, play direct from your streaming app, and it could be a 4.5-star product.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tested the Edifier MR5 — and they’re budget-friendly studio monitors with energy and clarity ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Edifier MR5 are some of the best budget-friendly studio monitors I’ve ever tested, and certainly some of the <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html">best speakers</a> you can buy these days. Although they’re not <em>quite</em> as precise as pricier alternatives — think the <a href="https://www.tomsguide.com/audio/speakers/cambridge-audio-lr-s-review">Cambridge L/R S</a> ($459) or Dali Kupid ($600), they’re excellent for well under $400. </p><p>With a veritable feast of connectivity types — we’re talking Aux, RCA, TRS, XLR, and Bluetooth — and a litany of customization options in the Edifier ConneX app, these monitors will be great for any content creator, music producer, or even casual listener. Heavier music fans might be better with the much cheaper Edifier R1280T or the Edifier S880DB MKII, but acoustic, pop, and electronic fans need look no further. </p><p>So, are the Edifier MR5 worth their salt, or can you find better seasoning elsewhere? Short answer: worth it. But to find out the full story, keep reading this Edifier MR5 review.</p><h2 class="article-body__section" id="section-edifier-mr5-review-specs"><span>Edifier MR5 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F5B73RGT" target="_blank" rel="nofollow">$349</a> /  <a href="https://www.amazon.co.uk/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F28FTWVQ" target="_blank" rel="nofollow">£279</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, AUX, RCA, TRS, XLR</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response rate</strong></p></td><td  ><p>46Hz - 40,000</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer out</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>22.9 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>11 x 6.3 x 10.4 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, white</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-edifier-mr5-review-price-availability"><span>Edifier MR5 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5iYgLVgd7aPkmPfARZDTz" name="Edifier_MR5_ 4.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/5iYgLVgd7aPkmPfARZDTz.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Edifier MR5 are <a href="https://www.amazon.com/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F5B73RGT" target="_blank" rel="nofollow">$349 from Amazon U.S.</a> and <a href="https://www.amazon.co.uk/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F28FTWVQ" target="_blank" rel="nofollow">£279 from Amazon U.K</a>. This makes them considerably more expensive than the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review">Edifier R1280T</a> ($149), my personal turntable speakers, and the <a href="https://www.tomsguide.com/audio/speakers/edifier-m60-review">Edifier M60</a> ($199). If you’re on a budget, I’d recommend getting one of those speakers instead — they’re cheaper, but still sound awesome. That’s not to say the MR5 aren’t worth it — they are — but every penny helps, right?</p><p>I’ve also tested the mid-range<a href="https://www.tomsguide.com/audio/speakers/cambridge-audio-lr-s-review"> Cambridge L/R S</a> ($549) and the premium-on-a-budget <a href="https://www.tomsguide.com/audio/speakers/edifier-s880db-mkii-review">Edifier S880DB MKII</a> ($449), both of which are your more punchy, louder speakers rather than the accurate studio-monitor-sound-profile of the MR5.</p><h2 class="article-body__section" id="section-edifier-mr5-review-design"><span>Edifier MR5 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="D5WPNjJuEE5KWF5CephEB" name="Edifier_MR5_ 1.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/D5WPNjJuEE5KWF5CephEB.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Edifier MR5 look like your standard Edifier speakers. They’re quite large, at 10 inches high, 6 inches wide, and 11 inches deep. I wouldn’t recommend them for small setups or for people who value a pretty speaker. </p><p>They don’t look as striking as the Edifier S880DB MKII, so I’d recommend those if you want an eye-catching speaker. Alternatively, I’d recommend the <a href="https://www.amazon.com/DALI-Premium-Compact-Bookshelf-Speakers/dp/B0FTK9BRTS" target="_blank" rel="nofollow">DALI Kupid</a> ($600) (but you need an AVR), or the <a href="https://www.tomsguide.com/audio/speakers/i-just-tested-the-best-desktop-speakers-ive-heard-in-years-and-they-take-up-very-little-room">Kanto UKI</a> ($269). </p><h2 id="controls-7">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bKpeRwvYuxg8sNqqCuhHC3" name="Edifier_MR5_ 5.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/bKpeRwvYuxg8sNqqCuhHC3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The MR5 have just one on-speaker control. You’ve got the volume dial on the front of the speaker and a headphone output. Of course, you can also control the speakers via Edifier’s ConneX app, which I’ll discuss in more detail below. </p><p>You’ve got a treble and bass (lovingly named “high” and “low”) on the back of the speaker, but I’d like to see these knobs on the front in future. It’s quite annoying to turn the speakers around just to increase bass. </p><h2 id="connectivity-4">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SwTZL3SEzof2ekbNjWyMM3" name="Edifier_MR5_ 7.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/SwTZL3SEzof2ekbNjWyMM3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>If you need studio monitors that can do it all, the MR5 might just be it. On top of the standard RCA and aux connectivity ports, the MR5 also have Bluetooth, RCA, and even XLR. This is ideal for studio musicians or home producers who need the highest possible quality. If you’re using broadcast mics, then you’ll need an XLR connection for the best quality. Unfortunately, the MR5 don’t have USB-C listening.</p><p>I found connecting to Bluetooth and Aux easy. There’s really nothing else for me to say! These speakers work as advertised. </p><p>The array of connectivity types might be a little <em>too</em> much for casual home listeners — in that case, I’d recommend the aforementioned R1280T. </p><h2 class="article-body__section" id="section-edifier-mr5-review-app"><span>Edifier MR5 review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Go6SUb9mc2BApCPVqACUQ" name="Edifier_MR5_ 9.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Go6SUb9mc2BApCPVqACUQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As with other Edifier products like the M60 and the S880DB MKII, the MR5 are compatible with the ConneX app. You have to connect via Bluetooth to be able to control the speakers, but the speakers should remember your settings for analog listening sessions. When I first unboxed the speakers, I had to update the firmware. This took 10 minutes, so bear in mind if you wanted to get listening straight away. </p><p>In terms of the app functions, there are actually some unique features. You can customize a 9-band EQ, choose between ‘Music’ or ‘Monitor’ EQs, and calibrate the room with the “Acoustic Space” configuration (decrease decibel level to compensate for wall placement). You can also configure desktop placement and cut-off frequencies from 20Hz to 100Hz. I didn’t notice a huge difference when I activated “Desktop control”, but “Acoustic Space” did make a difference to the bass. I kept it on 0dB. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xEWZ4YasmKRRmz8en3uHY9" name="edifier mr5 screenshots" alt="three screenshots from the edifier connex app showing the customization options" src="https://cdn.mos.cms.futurecdn.net/xEWZ4YasmKRRmz8en3uHY9.png" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Edifier / Tom's Guide)</span></figcaption></figure><p>As you can see, these functions are less gimmicky than those on the S880DB MKII, so I would actually recommend downloading this app and having a play around. </p><h2 class="article-body__section" id="section-edifier-mr5-review-sound-quality"><span>Edifier MR5 review: Sound quality</span></h2><p>To test sound quality, I listened to a variety of genres on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for audiophiles. I listened via Aux and Bluetooth. Of course, Aux provided the most “accurate” sound, but Bluetooth sounded good, too. </p><p>I played “Atomic” by Blondie via Bluetooth. The soundscape was incredibly wide. The opening guitars and synths sounded expansive and detailed, and the flitting keyboard notes darted around the guitar melody with verve. Debbie Harry’s voice sounded fantastic; clear, detailed, and sonorous. There was no pinching at the top of the treble or in the cymbals. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VagBBDiKkcr9BGe8euECVo" name="Edifier_MR5_ 11.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/VagBBDiKkcr9BGe8euECVo.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>After, I played “The Nightlife” by Honey Dijon. This house track is built over a swallowed synth motif, which rolls across the soundscape in what <em>sounds</em> like a full circle. The stereo imaging on these speakers is fantastic. I could feel the synth moving from left to right, up, down, and back again without moving my head. The treble did sound a little too bright so I reduced the “high” knob, which helped. </p><p>Lastly, I played “Citizen Erased” by Muse. This episodic rock track has a duplicate guitar + bass riff, so is great for testing heavy instrument separation. The percussion sounded great, with a bright snare drum and twinkling cymbals. The sludgy guitars weren’t as separated as I’d like, but the synth alarm sounds flickered across the soundscape like fireflies. I’d get these speakers if you</p><p>Although everything sounded <em>good</em>, and I’d gladly use these speakers as my main speakers at home. I’d recommend these speakers more for electronic, pop, or acoustic fans than for messy guitar music. </p><h2 class="article-body__section" id="section-edifier-mr5-review-verdict"><span>Edifier MR5 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="V9wyBBiSTARr9tUtwT36f" name="Edifier_MR5_.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/V9wyBBiSTARr9tUtwT36f.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Although somewhat ugly, the Edifier MR5 are great midrange speakers with good spaciousness and clarity. On top of having fantastic sound, they also boast a veritable feast of connectivity options — XLR, RCA, TRS, Aux, and Bluetooth. Hello? Did someone say spoiled? We’re eating tonight, guys. </p><p>There’s also basic room calibration in the form of “Acoustic Space” and “Desktop Control”. Obviously, if you want true room calibration, you’ll want to check out the Dirac-powered <a href="https://www.amazon.com/Klipsch-Sevens-Powered-Speakers-Walnut/dp/B0GLQMSNB6">Klipsch The Sevens II</a> ($2,000), but this is great for $300. </p><p>Would I buy these speakers? If I had a large desk space, 100%. The sound is rich and clear, and although some heavier tracks weren’t as detailed as I’d like, for electronic, pop, and acoustic, these studio monitors are fantastic. The MR5 are another home run from Edifier. When will it end? </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/speakers/edifier-mr5-review</link>
                                                                            <description>
                            <![CDATA[ The Edifier MR5 are budget-slash-midrange studio monitors with a huge five connectivity types, great sound quality, and actually useful app functionality. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">NoEtRD5dNuhDHgmLGkVjtY</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/q6omfm4gJQSx6mffv8tFY-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 17 Jul 2026 17:58:40 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/q6omfm4gJQSx6mffv8tFY-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom&#039;s guide background]]></media:description>                                                            <media:text><![CDATA[the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom&#039;s guide background]]></media:text>
                                <media:title type="plain"><![CDATA[the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom&#039;s guide background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/q6omfm4gJQSx6mffv8tFY-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Edifier MR5 are some of the best budget-friendly studio monitors I’ve ever tested, and certainly some of the <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html">best speakers</a> you can buy these days. Although they’re not <em>quite</em> as precise as pricier alternatives — think the <a href="https://www.tomsguide.com/audio/speakers/cambridge-audio-lr-s-review">Cambridge L/R S</a> ($459) or Dali Kupid ($600), they’re excellent for well under $400. </p><p>With a veritable feast of connectivity types — we’re talking Aux, RCA, TRS, XLR, and Bluetooth — and a litany of customization options in the Edifier ConneX app, these monitors will be great for any content creator, music producer, or even casual listener. Heavier music fans might be better with the much cheaper Edifier R1280T or the Edifier S880DB MKII, but acoustic, pop, and electronic fans need look no further. </p><p>So, are the Edifier MR5 worth their salt, or can you find better seasoning elsewhere? Short answer: worth it. But to find out the full story, keep reading this Edifier MR5 review.</p><h2 class="article-body__section" id="section-edifier-mr5-review-specs"><span>Edifier MR5 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F5B73RGT" target="_blank" rel="nofollow">$349</a> /  <a href="https://www.amazon.co.uk/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F28FTWVQ" target="_blank" rel="nofollow">£279</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, AUX, RCA, TRS, XLR</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response rate</strong></p></td><td  ><p>46Hz - 40,000</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer out</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>22.9 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>11 x 6.3 x 10.4 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, white</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-edifier-mr5-review-price-availability"><span>Edifier MR5 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5iYgLVgd7aPkmPfARZDTz" name="Edifier_MR5_ 4.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/5iYgLVgd7aPkmPfARZDTz.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Edifier MR5 are <a href="https://www.amazon.com/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F5B73RGT" target="_blank" rel="nofollow">$349 from Amazon U.S.</a> and <a href="https://www.amazon.co.uk/Edifier-MR5-Bookshelf-Bluetooth-Compensation/dp/B0F28FTWVQ" target="_blank" rel="nofollow">£279 from Amazon U.K</a>. This makes them considerably more expensive than the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review">Edifier R1280T</a> ($149), my personal turntable speakers, and the <a href="https://www.tomsguide.com/audio/speakers/edifier-m60-review">Edifier M60</a> ($199). If you’re on a budget, I’d recommend getting one of those speakers instead — they’re cheaper, but still sound awesome. That’s not to say the MR5 aren’t worth it — they are — but every penny helps, right?</p><p>I’ve also tested the mid-range<a href="https://www.tomsguide.com/audio/speakers/cambridge-audio-lr-s-review"> Cambridge L/R S</a> ($549) and the premium-on-a-budget <a href="https://www.tomsguide.com/audio/speakers/edifier-s880db-mkii-review">Edifier S880DB MKII</a> ($449), both of which are your more punchy, louder speakers rather than the accurate studio-monitor-sound-profile of the MR5.</p><h2 class="article-body__section" id="section-edifier-mr5-review-design"><span>Edifier MR5 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="D5WPNjJuEE5KWF5CephEB" name="Edifier_MR5_ 1.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/D5WPNjJuEE5KWF5CephEB.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Edifier MR5 look like your standard Edifier speakers. They’re quite large, at 10 inches high, 6 inches wide, and 11 inches deep. I wouldn’t recommend them for small setups or for people who value a pretty speaker. </p><p>They don’t look as striking as the Edifier S880DB MKII, so I’d recommend those if you want an eye-catching speaker. Alternatively, I’d recommend the <a href="https://www.amazon.com/DALI-Premium-Compact-Bookshelf-Speakers/dp/B0FTK9BRTS" target="_blank" rel="nofollow">DALI Kupid</a> ($600) (but you need an AVR), or the <a href="https://www.tomsguide.com/audio/speakers/i-just-tested-the-best-desktop-speakers-ive-heard-in-years-and-they-take-up-very-little-room">Kanto UKI</a> ($269). </p><h2 id="controls-7">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bKpeRwvYuxg8sNqqCuhHC3" name="Edifier_MR5_ 5.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/bKpeRwvYuxg8sNqqCuhHC3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The MR5 have just one on-speaker control. You’ve got the volume dial on the front of the speaker and a headphone output. Of course, you can also control the speakers via Edifier’s ConneX app, which I’ll discuss in more detail below. </p><p>You’ve got a treble and bass (lovingly named “high” and “low”) on the back of the speaker, but I’d like to see these knobs on the front in future. It’s quite annoying to turn the speakers around just to increase bass. </p><h2 id="connectivity-4">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SwTZL3SEzof2ekbNjWyMM3" name="Edifier_MR5_ 7.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/SwTZL3SEzof2ekbNjWyMM3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>If you need studio monitors that can do it all, the MR5 might just be it. On top of the standard RCA and aux connectivity ports, the MR5 also have Bluetooth, RCA, and even XLR. This is ideal for studio musicians or home producers who need the highest possible quality. If you’re using broadcast mics, then you’ll need an XLR connection for the best quality. Unfortunately, the MR5 don’t have USB-C listening.</p><p>I found connecting to Bluetooth and Aux easy. There’s really nothing else for me to say! These speakers work as advertised. </p><p>The array of connectivity types might be a little <em>too</em> much for casual home listeners — in that case, I’d recommend the aforementioned R1280T. </p><h2 class="article-body__section" id="section-edifier-mr5-review-app"><span>Edifier MR5 review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Go6SUb9mc2BApCPVqACUQ" name="Edifier_MR5_ 9.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Go6SUb9mc2BApCPVqACUQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As with other Edifier products like the M60 and the S880DB MKII, the MR5 are compatible with the ConneX app. You have to connect via Bluetooth to be able to control the speakers, but the speakers should remember your settings for analog listening sessions. When I first unboxed the speakers, I had to update the firmware. This took 10 minutes, so bear in mind if you wanted to get listening straight away. </p><p>In terms of the app functions, there are actually some unique features. You can customize a 9-band EQ, choose between ‘Music’ or ‘Monitor’ EQs, and calibrate the room with the “Acoustic Space” configuration (decrease decibel level to compensate for wall placement). You can also configure desktop placement and cut-off frequencies from 20Hz to 100Hz. I didn’t notice a huge difference when I activated “Desktop control”, but “Acoustic Space” did make a difference to the bass. I kept it on 0dB. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xEWZ4YasmKRRmz8en3uHY9" name="edifier mr5 screenshots" alt="three screenshots from the edifier connex app showing the customization options" src="https://cdn.mos.cms.futurecdn.net/xEWZ4YasmKRRmz8en3uHY9.png" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Edifier / Tom's Guide)</span></figcaption></figure><p>As you can see, these functions are less gimmicky than those on the S880DB MKII, so I would actually recommend downloading this app and having a play around. </p><h2 class="article-body__section" id="section-edifier-mr5-review-sound-quality"><span>Edifier MR5 review: Sound quality</span></h2><p>To test sound quality, I listened to a variety of genres on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for audiophiles. I listened via Aux and Bluetooth. Of course, Aux provided the most “accurate” sound, but Bluetooth sounded good, too. </p><p>I played “Atomic” by Blondie via Bluetooth. The soundscape was incredibly wide. The opening guitars and synths sounded expansive and detailed, and the flitting keyboard notes darted around the guitar melody with verve. Debbie Harry’s voice sounded fantastic; clear, detailed, and sonorous. There was no pinching at the top of the treble or in the cymbals. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VagBBDiKkcr9BGe8euECVo" name="Edifier_MR5_ 11.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/VagBBDiKkcr9BGe8euECVo.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>After, I played “The Nightlife” by Honey Dijon. This house track is built over a swallowed synth motif, which rolls across the soundscape in what <em>sounds</em> like a full circle. The stereo imaging on these speakers is fantastic. I could feel the synth moving from left to right, up, down, and back again without moving my head. The treble did sound a little too bright so I reduced the “high” knob, which helped. </p><p>Lastly, I played “Citizen Erased” by Muse. This episodic rock track has a duplicate guitar + bass riff, so is great for testing heavy instrument separation. The percussion sounded great, with a bright snare drum and twinkling cymbals. The sludgy guitars weren’t as separated as I’d like, but the synth alarm sounds flickered across the soundscape like fireflies. I’d get these speakers if you</p><p>Although everything sounded <em>good</em>, and I’d gladly use these speakers as my main speakers at home. I’d recommend these speakers more for electronic, pop, or acoustic fans than for messy guitar music. </p><h2 class="article-body__section" id="section-edifier-mr5-review-verdict"><span>Edifier MR5 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="V9wyBBiSTARr9tUtwT36f" name="Edifier_MR5_.JPG" alt="the Edifier MR5 studio monitors bookshelf speakers photographed with the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/V9wyBBiSTARr9tUtwT36f.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Although somewhat ugly, the Edifier MR5 are great midrange speakers with good spaciousness and clarity. On top of having fantastic sound, they also boast a veritable feast of connectivity options — XLR, RCA, TRS, Aux, and Bluetooth. Hello? Did someone say spoiled? We’re eating tonight, guys. </p><p>There’s also basic room calibration in the form of “Acoustic Space” and “Desktop Control”. Obviously, if you want true room calibration, you’ll want to check out the Dirac-powered <a href="https://www.amazon.com/Klipsch-Sevens-Powered-Speakers-Walnut/dp/B0GLQMSNB6">Klipsch The Sevens II</a> ($2,000), but this is great for $300. </p><p>Would I buy these speakers? If I had a large desk space, 100%. The sound is rich and clear, and although some heavier tracks weren’t as detailed as I’d like, for electronic, pop, and acoustic, these studio monitors are fantastic. The MR5 are another home run from Edifier. When will it end? </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ 11 best Bluetooth speaker deals under $100 at Amazon — save up to 50% on Bose, JBL, Anker and more ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Summer is here — and with it comes plenty of beach days and pool parties! If you need a bluetooth speaker that can keep up with all the fun vibes, you've come to the right place. </p><p><a href="https://www.amazon.com/s?k=speakers&i=electronics" target="_blank" rel="nofollow">I've been scouring Amazon for affordable speakers</a> and to my surprise, I found a bunch that are under $100. Even better? They come from top brands like Bose, JBL and Anker. Plus, several hold top spots on Tom's Guide's list of <a href="https://www.tomsguide.com/best-picks/best-waterproof-speakers">best waterproof bluetooth speakers</a> we've tested.</p><p>If you're ready to upgrade your bluetooth speaker without depleting your wallet, keep scrolling to check out my favorite Amazon speaker deals starting at just $22. </p><h3 class="article-body__section" id="section-quick-links"><span>Quick Links </span></h3><ul><li><a href="https://www.amazon.com/s?k=speakers&i=electronics" target="_blank" rel="nofollow"><strong>shop all speaker deals on Amazon</strong></a></li><li><strong>Tribit PocketGo: </strong><a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><strong>was $29 now $22</strong></a></li><li><strong>Anker Soundcore 2: </strong><a href="https://www.amazon.com/Anker-Soundcore-Portable-Bluetooth-Waterproof/dp/B0FRRMD96F" target="_blank" rel="nofollow"><strong>was $45 now $33</strong></a></li><li><strong>JBL Go 5: </strong><a href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><strong>was $54 now $44</strong></a></li><li><strong>Ultimate Ears WonderBoom 4: </strong><a href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow"><strong>was $99 now $51</strong></a></li><li><strong>Bose SoundLink Micro: </strong><a href="https://www.amazon.com/dp/B0FDLCCFQP?" target="_blank" rel="nofollow"><strong>was $129 now $79</strong></a></li><li><strong>JBL Flip 6: </strong><a href="https://www.amazon.com/dp/B09GJVTRNZ?" target="_blank" rel="nofollow"><strong>was $129 now $83</strong></a></li><li><strong>Soundcore Boom 2: </strong><a href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><strong>was $129 now $90</strong></a></li><li><strong>Bose SoundLink Flex (Gen 2): </strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FGY8BBVP?" target="_blank" rel="nofollow"><strong>was $149 now $99</strong></a></li></ul><h3 class="article-body__section" id="section-best-bluetooth-speaker-deals"><span>Best Bluetooth Speaker Deals</span></h3><div class="product"><a data-dimension112="5943348c-811f-11f1-bd33-37cba0d9ad5b" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$22" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="XnqELzjemVWDKqXxoESeBh" name="tribit-pocketgo-bluetooth-shower-speaker-e7119067-af9e-4c09-9feb-6f1f593c89d6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XnqELzjemVWDKqXxoESeBh.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review" data-dimension112="5943348c-811f-11f1-bd33-37cba0d9ad5b" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$22">Tribit PocketGo</a> will be your new best friend. For less than $30, this Bluetooth speaker packs one heck of a punch in the sound department. On top of that, it’s the ultimate rugged speaker. Its IP68 waterproofing and military-grade drop protection make it perfect for long hikes in unpredictable weather. And I do mean <em>long</em> hikes because the speaker has a total battery life of 20 hours, making it ideal for a weekend camping trip. <a class="view-deal button" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow" data-dimension112="5943348c-811f-11f1-bd33-37cba0d9ad5b" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$22">View Deal</a></p></div><div class="product"><a data-dimension112="594334f0-811f-11f1-a74b-9947fb55ede7" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$29" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1480px;"><p class="vanilla-image-block" style="padding-top:101.35%;"><img id="iRvkqAH3YBcTTf4zRa7AdC" name="Tribit-StormBox-Mini-Plus-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/iRvkqAH3YBcTTf4zRa7AdC.jpg" mos="" align="middle" fullscreen="" width="1480" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review" data-dimension112="594334f0-811f-11f1-a74b-9947fb55ede7" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$29">Tribit StormBox Mini+</a> boasts powerful bass response and detailed vocals. It also sports reactive LED lighting that’s fun to watch, and IPX7 waterproofing so it can be used poolside or in the shower. For the money, I recommend buying two so you can take advantage of the dreamy stereo sound.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow" data-dimension112="594334f0-811f-11f1-a74b-9947fb55ede7" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$29">View Deal</a></p></div><div class="product"><a data-dimension112="5943355e-811f-11f1-8e6f-b5d6c32727f8" data-action="Deal Block" data-label="Soundcore 2" data-dimension48="Soundcore 2" data-dimension25="$33" href="https://www.amazon.com/Anker-Soundcore-Portable-Bluetooth-Waterproof/dp/B0FRRMD96F" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:48.00%;"><img id="8BpRx7CmiYxHLqkqkGhn7o" name="anker-soundcore-2-portable-bluetooth-spe-135919d3-382b-433e-a4f8-1715f6ec9dd3.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/8BpRx7CmiYxHLqkqkGhn7o.jpg" mos="" align="middle" fullscreen="" width="500" height="240" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I had the Anker Soundcore 2 for many years (it was originally released in 2017), but it still manages to hold up well today. Though you don't get the same booming bass as the offerings from Tribit above, the 24-hour battery life means it's one I've always been able to rely on. Just because a speaker is a little older doesn't mean it's <em>actually</em> outdated. <a class="view-deal button" href="https://www.amazon.com/Anker-Soundcore-Portable-Bluetooth-Waterproof/dp/B0FRRMD96F" target="_blank" rel="nofollow" data-dimension112="5943355e-811f-11f1-8e6f-b5d6c32727f8" data-action="Deal Block" data-label="Soundcore 2" data-dimension48="Soundcore 2" data-dimension25="$33">View Deal</a></p></div><div class="product"><a data-dimension112="594335c2-811f-11f1-86d0-9bb5f4f4297d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="YHNWrEwxj5uzgYGXCTeiJ6" name="Go5-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/YHNWrEwxj5uzgYGXCTeiJ6.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review" data-dimension112="594335c2-811f-11f1-86d0-9bb5f4f4297d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">JBL Go 5</a> boasts a wide soundstage packed into a compact, pocketable body. It looks great, sounds even better, and can be customized via a user-friendly companion app. It’s IP68-rated, so it’s highly waterproof, dustproof and drop-proof too.<a class="view-deal button" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow" data-dimension112="594335c2-811f-11f1-86d0-9bb5f4f4297d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">View Deal</a></p></div><div class="product"><a data-dimension112="59433626-811f-11f1-a1ea-058e50cfad1a" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1110px;"><p class="vanilla-image-block" style="padding-top:118.65%;"><img id="dsAxZA3JpSDwfF643VP3eB" name="Micro 3 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/dsAxZA3JpSDwfF643VP3eB.jpg" mos="" align="middle" fullscreen="" width="1110" height="1317" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review" data-dimension112="59433626-811f-11f1-a1ea-058e50cfad1a" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49">Tribit StormBox Micro 3 </a>offers an IP68 rating, a companion app for EQ customization (although some settings cause compression), and simple controls that make the speaker a breeze to use. It has 24 hours of battery life and can even be used as a portable charger. But most importantly, the audio performance really is excellent.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow" data-dimension112="59433626-811f-11f1-a1ea-058e50cfad1a" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49">View Deal</a></p></div><div class="product"><a data-dimension112="5943368a-811f-11f1-82aa-759414816d13" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1425px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="D7QYGUpe98kCsZkVddg4oV" name="Ultimate Ears Wonderboom 4.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/D7QYGUpe98kCsZkVddg4oV.jpg" mos="" align="middle" fullscreen="" width="1425" height="1425" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The mighty and portable Wonderboom 4 is a great outdoor speaker that goes louder than it has any right to. In our <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ue-wonderboom-4-review" data-dimension112="5943368a-811f-11f1-82aa-759414816d13" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">Ultimate Ears WonderBoom 4 review</a>, our reviewer loved its compact size, very good 16-hour battery life and slick rugged looks. It's IP67 water and dust-resistant as well, so you can really take it anywhere to listen to your tunes.<a class="view-deal button" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow" data-dimension112="5943368a-811f-11f1-82aa-759414816d13" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">View Deal</a></p></div><div class="product"><a data-dimension112="4a7e6878-8123-11f1-af3e-05e77453d77d" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$59" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1115px;"><p class="vanilla-image-block" style="padding-top:49.42%;"><img id="U4eB79RfdkjMCrjY6xNuFm" name="EarFun-UBoomL-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/U4eB79RfdkjMCrjY6xNuFm.png" mos="" align="middle" fullscreen="" width="1115" height="551" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/speakers/bluetooth-speakers/earfun-uboom-l-review" data-dimension112="4a7e6878-8123-11f1-af3e-05e77453d77d" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$59">EarFun UBoom L</a> offers big sound on a budget. This portable speaker produces powerful bass, clear vocals and well-balanced sound across a range of genres. With a dedicated outdoor mode and an IP67 resistance rating, the speaker can soundtrack pool parties and hikes. It has great battery life, and the customizable EQ via the user-friendly companion app works really well.<a class="view-deal button" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow" data-dimension112="4a7e6878-8123-11f1-af3e-05e77453d77d" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$59">View Deal</a></p></div><div class="product"><a data-dimension112="594336ee-811f-11f1-8cfc-1d3c74cc3851" data-action="Deal Block" data-label="SoundLink Micro" data-dimension48="SoundLink Micro" data-dimension25="$79" href="https://www.amazon.com/dp/B0FDLCCFQP?" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1105px;"><p class="vanilla-image-block" style="padding-top:103.98%;"><img id="YTZZQubUxh8Cg22rMiKso7" name="Bose soundlink" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/YTZZQubUxh8Cg22rMiKso7.jpg" mos="" align="middle" fullscreen="" width="1105" height="1149" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The SoundLink Micro is a compact, portable speaker you can take on the road. The Bluetooth speaker is waterproof (IPX7 certified) and offers up to 6 hours of play time off a single charge. It can also be paired with another SoundLink speaker to create a stereo mode.<a class="view-deal button" href="https://www.amazon.com/dp/B0FDLCCFQP?" target="_blank" rel="nofollow" data-dimension112="594336ee-811f-11f1-8cfc-1d3c74cc3851" data-action="Deal Block" data-label="SoundLink Micro" data-dimension48="SoundLink Micro" data-dimension25="$79">View Deal</a></p></div><div class="product"><a data-dimension112="594337c0-811f-11f1-8aeb-03772b602a7f" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$83" href="https://www.amazon.com/dp/B09GJVTRNZ?" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="33AJemCSzK8fNYEhkUfBtm" name="JBL Flip 6" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/33AJemCSzK8fNYEhkUfBtm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>JBL just doesn't miss with its Bluetooth speakers. Although the Flip 7 came out earlier this year, the Flip 6 shouldn't be forgotten about. It's now on sale and an awesome deal for this pocket-sized 30W speaker. It's IP67 rated and has a 12 hour battery life, meaning it's perfect to chuck in your backpack for all day outdoor listening. <a class="view-deal button" href="https://www.amazon.com/dp/B09GJVTRNZ?" target="_blank" rel="nofollow" data-dimension112="594337c0-811f-11f1-8aeb-03772b602a7f" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$83">View Deal</a></p></div><div class="product"><a data-dimension112="59433752-811f-11f1-a286-09509732d7c0" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$90" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="3CvgYRTFMbxneUZcepKkae" name="soundcore boom 2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/3CvgYRTFMbxneUZcepKkae.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A speaker of this caliber for just $90 is unheard of. The Boom 2 has a massive 80W power, meaning it will go <em>loud</em>. Like the much pricier JBL options, the Boom 2 has a 24 hour battery life and IP67 rating, so it'll be able to follow you anywhere. <a class="view-deal button" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow" data-dimension112="59433752-811f-11f1-a286-09509732d7c0" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$90">View Deal</a></p></div><div class="product"><a data-dimension112="5943381a-811f-11f1-8333-1faca2219b7a" data-action="Deal Block" data-label="SoundLink Flex (Gen 2)" data-dimension48="SoundLink Flex (Gen 2)" data-dimension25="$99" href="https://www.amazon.com/Bose-SoundLink-Flex-Bluetooth-Speaker/dp/B0FGY8BBVP/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1063px;"><p class="vanilla-image-block" style="padding-top:73.00%;"><img id="4oVcFP6PokTpSnss5yrCLB" name="Bose Soundlink Flex" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/4oVcFP6PokTpSnss5yrCLB.jpg" mos="" align="middle" fullscreen="" width="1063" height="776" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Bose-SoundLink-Flex-Bluetooth-Speaker/dp/B0FGY8BBVP/" target="_blank" rel="nofollow" data-dimension112="5943381a-811f-11f1-8333-1faca2219b7a" data-action="Deal Block" data-label="SoundLink Flex (Gen 2)" data-dimension48="SoundLink Flex (Gen 2)" data-dimension25="$99">SoundLink Flex (Gen 2): was $159 now $99</a></strong><br>The new Bose SoundLink (2nd gen) is a compact and powerful Bluetooth speaker with a durable design. This 1.2-pound speaker is water and dust proof and delivered great sound quality in our <a href="https://www.tomsguide.com/audio/bose-soundlink-flex-2-review" target="_blank">SoundLink Flex gen 2 review</a>. With 12 hours of battery life and $59 off, you have yourself a great deal.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Flex-Bluetooth-Speaker/dp/B0FGY8BBVP/" target="_blank" rel="nofollow" data-dimension112="5943381a-811f-11f1-8333-1faca2219b7a" data-action="Deal Block" data-label="SoundLink Flex (Gen 2)" data-dimension48="SoundLink Flex (Gen 2)" data-dimension25="$99">View Deal</a></p></div><h3 class="article-body__section" id="section-shop-more-deals"><span>Shop more deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Speakers&brands=Sonos%2CAnker%2CBeats&min_discount_ratio=0.95&offer_type=all&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }        .tg-df-container .tg-df-carousel-scroll-left,    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-left { left: 8px; }    .tg-df-container .tg-df-carousel-scroll-right { right: 8px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left { left: 0px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { right: 0px; }    .tg-df-carousel-filters-outer { margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; }    @container tg-df (max-width: 599px) { .tg-df-carousel-filters-outer { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; } }        .tg-df-container .tg-df-carousel-scroll-left:hover,    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right {      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right { right: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-right { right: 0; }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left:hover,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-outer" style="position: relative;">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>        \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3C/div>    \x3C/div>    \x3C/div>      \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>      \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }          const website = params.get('website') || 'tomsguide';          this.website = website;          if (website === 'techradar') {            const squadHeader = this.root.querySelector('.tg-df-savings-squad-header');            if (squadHeader) {               const pic = squadHeader.querySelector('picture');               if (pic) pic.style.display = 'none';            }            const style = document.createElement('style');            style.innerHTML = `              .tg-df-container .hawk-affiliate-link-deal-button, .tg-df-container .tg-df-card-cta { background-color: #5DAF08 !important; }              .tg-df-container .hawk-affiliate-link-deal-button:hover, .tg-df-container .tg-df-card-cta:hover { background-color: #4a8c06 !important; }            `;            this.root.appendChild(style);          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }          this.setupScrollListeners();        }        setupScrollListeners() {          const containers = [             this.root.querySelector('.tg-df-carousel-roundels'),             this.root.querySelector('.tg-df-carousel-filters-wrap'),             this.root.querySelector('#tg-df-grid')          ];                    containers.forEach(container => {             if (!container) return;                          const checkScroll = () => {                if (!container.parentElement) return;                const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left');                const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right');                                if (leftBtn) {                   if (container.scrollLeft <= 5) leftBtn.style.display = 'none';                   else leftBtn.style.display = 'flex';                }                                if (rightBtn) {                   if (container.scrollWidth <= container.clientWidth) {                       rightBtn.style.display = 'none';                   } else if (container.scrollLeft >= container.scrollWidth - container.clientWidth - 5) {                       rightBtn.style.display = 'none';                   } else {                       rightBtn.style.display = 'flex';                   }                }             };                          container.addEventListener('scroll', checkScroll);             checkScroll();                          window.addEventListener('resize', checkScroll);                          const observer = new MutationObserver(checkScroll);             observer.observe(container, { childList: true, subtree: true, characterData: false });          });        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getRowsLimit() {          if (this.rowsSelect && this.rowsSelect.value) return parseInt(this.rowsSelect.value, 10);          let configSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {             configSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {             configSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(configSource);          if (params.has('rows')) return parseInt(params.get('rows'), 10) || 12;          return 12;        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = this.getRowsLimit();          } else {             this.displayLimit += this.getRowsLimit();          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');             const encodedTag = encodeURIComponent(cleanTag);             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    url.searchParams.append('rows', this.getRowsLimit().toString());          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = this.getRowsLimit();          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map((v, idx) => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              const revenueIdVal = generateRevenueId(linkUrl, offerName, merchantName, null);              const rewrittenLinkUrl = rewriteAffiliateLink(linkUrl, area, revenueIdVal);              return `                \x3Ca href="${this.escapeHTML(rewrittenLinkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item"                  data-action="voucher-click"                  data-product-name="${this.escapeHTML(offerName)}"                  data-merchant-name="${this.escapeHTML(merchantName)}"                  data-analytics-id="${this.escapeHTML(offerObj.offer_id || offerObj.id || v.id || '')}"                  data-price=""                  data-previous-price=""                  data-original-link="${this.escapeHTML(linkUrl)}"                  data-revenue-id="${revenueIdVal}"                  data-index="${idx}"                  data-total="${offers.length}"                  data-in-stock="true"                  data-currency="USD"                  data-model-id="${this.escapeHTML(offerObj.model_id || v.model_id || offerObj.id || v.id || '')}"                  data-merchant-id="${this.escapeHTML(offerObj.merchant_id || offerObj.merchant?.id || '')}"                >                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });            // Attach voucher click tracking            const voucherBtns = content.querySelectorAll('[data-action="voucher-click"]');            voucherBtns.forEach(btn => {              btn.addEventListener('click', (e) => {                if (e.target.closest('[data-action="copy-code"]')) return;                                const productName = btn.getAttribute('data-product-name');                const merchantNameAttr = btn.getAttribute('data-merchant-name');                const productId = btn.getAttribute('data-analytics-id');                const price = parseFloat(btn.getAttribute('data-price')) || null;                const prevPriceStr = btn.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = btn.getAttribute('data-original-link');                const rewrittenLink = btn.getAttribute('href');                const revenueId = btn.getAttribute('data-revenue-id');                const index = parseInt(btn.getAttribute('data-index'), 10) || 0;                const inStock = btn.getAttribute('data-in-stock') === 'true';                const totalText = btn.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: btn.getAttribute('data-model-id') || null,                    matchId: btn.getAttribute('data-match-id') || null,                    brand: btn.getAttribute('data-model-brand') || null,                    parent: btn.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: btn.getAttribute('data-merchant-id') || null,                    network: btn.getAttribute('data-merchant-network') || null,                    url: btn.getAttribute('data-merchant-url') || null,                    name: merchantNameAttr                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: btn.getAttribute('data-currency') || 'USD'                };                if (typeof trackDealClick === 'function') {                  trackDealClick(trackingParams);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;          // Inject JSON-LD          try {            let targetNode = this.hostContainer || document.head;            let jsonLdScript = targetNode.querySelector('#tg-df-json-ld-' + this.widgetId);            if (!jsonLdScript) {                jsonLdScript = document.createElement('script');                jsonLdScript.type = 'application/ld+json';                jsonLdScript.id = 'tg-df-json-ld-' + this.widgetId;                targetNode.appendChild(jsonLdScript);            }            const jsonLdData = {              "@context": "https://schema.org",              "@type": "ItemList",              "itemListElement": displayDeals.slice(0, this.displayLimit).map((deal, index) => {                 let isoCurrency = "USD";                 if (deal.currency === '£') isoCurrency = "GBP";                 else if (deal.currency === '€') isoCurrency = "EUR";                 else if (deal.currency === 'A$') isoCurrency = "AUD";                 else if (deal.currency === 'CA$') isoCurrency = "CAD";                 const areaCode = typeof this.getAreaCode === 'function' ? this.getAreaCode() : 'US';                 const revenueId = typeof generateRevenueId === 'function' ? generateRevenueId(deal.url, deal.title, deal.merchant, null) : '';                 const rewrittenLink = typeof rewriteAffiliateLink === 'function' ? rewriteAffiliateLink(deal.url, areaCode, revenueId) : deal.url;                 return {                   "@type": "ListItem",                   "position": index + 1,                   "item": {                     "@type": "Product",                     "name": deal.title,                     "image": deal.image || "",                     "description": deal.description || "",                     "brand": {                       "@type": "Brand",                       "name": deal.brand || ""                     },                     "offers": {                       "@type": "Offer",                       "priceCurrency": isoCurrency,                       "price": deal.rawPrice || 0,                       "url": rewrittenLink,                       "seller": {                         "@type": "Organization",                         "name": deal.merchant || ""                       }                     }                   }                 };              }).filter(item => item.item.name)            };            jsonLdScript.textContent = JSON.stringify(jsonLdData);          } catch(e) { console.warn("JSON-LD generation failed", e); }                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let rightChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             let leftChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-left');             if (this.getViewMode() === 'carousel') {                 // The observer set up in setupScrollListeners handles visibility.                 if (rightChevron) rightChevron.style.display = 'flex';                 if (leftChevron) leftChevron.style.display = 'none'; // reset correctly             } else {                 if (rightChevron) rightChevron.style.display = 'none';                 if (leftChevron) leftChevron.style.display = 'none';             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += this.getRowsLimit();                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');               document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/11-best-bluetooth-speaker-deals-under-usd100-at-amazon-save-up-to-50-percent-on-bose-jbl-anker-and-more</link>
                                                                            <description>
                            <![CDATA[ Amazon is slashing the prices on bluetooth speakers for summer. Here are my favorite deals under $100 from Bose, JBL, Anker and more. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">L8DQQXPbnozs5wUvHp4UtH</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/EVEpowToZkhYE7gEKKFyiF-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 16 Jul 2026 15:35:46 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                                    <dc:creator><![CDATA[ Olivia Halevy ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/YAg7Fmh9dGKbg3hULBMWiU.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Olivia is a Deals Writer at Tom’s Guide. She loves all things lifestyle, shopping and sourcing the best deals from some of the top brands, retailers and websites around. From fashion and beauty to smart home and tech, she has an eye for finding practical, stylish, and reasonably-priced items that help make life simpler (and a little more aesthetically-pleasing).&amp;nbsp;Olivia has over five years of experience writing lifestyle, commerce, and branded content for &lt;a href=&quot;http://patch.com/&quot;&gt;Patch.com&lt;/a&gt;. She&#039;s covered major shopping holidays like Amazon Prime Day, Black Friday and Cyber Monday. In her free time, she enjoys trying out new exercise classes, binge-watching the latest viral TV shows, cooking, spending time with family and taking long walks with her dog, Gus.&amp;nbsp;&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/EVEpowToZkhYE7gEKKFyiF-1280-80.jpg">
                                                            <media:credit><![CDATA[Amazon / edited by Gemini]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Amazon deals]]></media:description>                                                            <media:text><![CDATA[Amazon deals]]></media:text>
                                <media:title type="plain"><![CDATA[Amazon deals]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/EVEpowToZkhYE7gEKKFyiF-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Summer is here — and with it comes plenty of beach days and pool parties! If you need a bluetooth speaker that can keep up with all the fun vibes, you've come to the right place. </p><p><a href="https://www.amazon.com/s?k=speakers&i=electronics" target="_blank" rel="nofollow">I've been scouring Amazon for affordable speakers</a> and to my surprise, I found a bunch that are under $100. Even better? They come from top brands like Bose, JBL and Anker. Plus, several hold top spots on Tom's Guide's list of <a href="https://www.tomsguide.com/best-picks/best-waterproof-speakers">best waterproof bluetooth speakers</a> we've tested.</p><p>If you're ready to upgrade your bluetooth speaker without depleting your wallet, keep scrolling to check out my favorite Amazon speaker deals starting at just $22. </p><h3 class="article-body__section" id="section-quick-links"><span>Quick Links </span></h3><ul><li><a href="https://www.amazon.com/s?k=speakers&i=electronics" target="_blank" rel="nofollow"><strong>shop all speaker deals on Amazon</strong></a></li><li><strong>Tribit PocketGo: </strong><a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><strong>was $29 now $22</strong></a></li><li><strong>Anker Soundcore 2: </strong><a href="https://www.amazon.com/Anker-Soundcore-Portable-Bluetooth-Waterproof/dp/B0FRRMD96F" target="_blank" rel="nofollow"><strong>was $45 now $33</strong></a></li><li><strong>JBL Go 5: </strong><a href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><strong>was $54 now $44</strong></a></li><li><strong>Ultimate Ears WonderBoom 4: </strong><a href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow"><strong>was $99 now $51</strong></a></li><li><strong>Bose SoundLink Micro: </strong><a href="https://www.amazon.com/dp/B0FDLCCFQP?" target="_blank" rel="nofollow"><strong>was $129 now $79</strong></a></li><li><strong>JBL Flip 6: </strong><a href="https://www.amazon.com/dp/B09GJVTRNZ?" target="_blank" rel="nofollow"><strong>was $129 now $83</strong></a></li><li><strong>Soundcore Boom 2: </strong><a href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><strong>was $129 now $90</strong></a></li><li><strong>Bose SoundLink Flex (Gen 2): </strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FGY8BBVP?" target="_blank" rel="nofollow"><strong>was $149 now $99</strong></a></li></ul><h3 class="article-body__section" id="section-best-bluetooth-speaker-deals"><span>Best Bluetooth Speaker Deals</span></h3><div class="product"><a data-dimension112="5943348c-811f-11f1-bd33-37cba0d9ad5b" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$22" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="XnqELzjemVWDKqXxoESeBh" name="tribit-pocketgo-bluetooth-shower-speaker-e7119067-af9e-4c09-9feb-6f1f593c89d6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XnqELzjemVWDKqXxoESeBh.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review" data-dimension112="5943348c-811f-11f1-bd33-37cba0d9ad5b" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$22">Tribit PocketGo</a> will be your new best friend. For less than $30, this Bluetooth speaker packs one heck of a punch in the sound department. On top of that, it’s the ultimate rugged speaker. Its IP68 waterproofing and military-grade drop protection make it perfect for long hikes in unpredictable weather. And I do mean <em>long</em> hikes because the speaker has a total battery life of 20 hours, making it ideal for a weekend camping trip. <a class="view-deal button" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow" data-dimension112="5943348c-811f-11f1-bd33-37cba0d9ad5b" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$22">View Deal</a></p></div><div class="product"><a data-dimension112="594334f0-811f-11f1-a74b-9947fb55ede7" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$29" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1480px;"><p class="vanilla-image-block" style="padding-top:101.35%;"><img id="iRvkqAH3YBcTTf4zRa7AdC" name="Tribit-StormBox-Mini-Plus-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/iRvkqAH3YBcTTf4zRa7AdC.jpg" mos="" align="middle" fullscreen="" width="1480" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review" data-dimension112="594334f0-811f-11f1-a74b-9947fb55ede7" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$29">Tribit StormBox Mini+</a> boasts powerful bass response and detailed vocals. It also sports reactive LED lighting that’s fun to watch, and IPX7 waterproofing so it can be used poolside or in the shower. For the money, I recommend buying two so you can take advantage of the dreamy stereo sound.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow" data-dimension112="594334f0-811f-11f1-a74b-9947fb55ede7" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$29">View Deal</a></p></div><div class="product"><a data-dimension112="5943355e-811f-11f1-8e6f-b5d6c32727f8" data-action="Deal Block" data-label="Soundcore 2" data-dimension48="Soundcore 2" data-dimension25="$33" href="https://www.amazon.com/Anker-Soundcore-Portable-Bluetooth-Waterproof/dp/B0FRRMD96F" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:48.00%;"><img id="8BpRx7CmiYxHLqkqkGhn7o" name="anker-soundcore-2-portable-bluetooth-spe-135919d3-382b-433e-a4f8-1715f6ec9dd3.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/8BpRx7CmiYxHLqkqkGhn7o.jpg" mos="" align="middle" fullscreen="" width="500" height="240" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I had the Anker Soundcore 2 for many years (it was originally released in 2017), but it still manages to hold up well today. Though you don't get the same booming bass as the offerings from Tribit above, the 24-hour battery life means it's one I've always been able to rely on. Just because a speaker is a little older doesn't mean it's <em>actually</em> outdated. <a class="view-deal button" href="https://www.amazon.com/Anker-Soundcore-Portable-Bluetooth-Waterproof/dp/B0FRRMD96F" target="_blank" rel="nofollow" data-dimension112="5943355e-811f-11f1-8e6f-b5d6c32727f8" data-action="Deal Block" data-label="Soundcore 2" data-dimension48="Soundcore 2" data-dimension25="$33">View Deal</a></p></div><div class="product"><a data-dimension112="594335c2-811f-11f1-86d0-9bb5f4f4297d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="YHNWrEwxj5uzgYGXCTeiJ6" name="Go5-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/YHNWrEwxj5uzgYGXCTeiJ6.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review" data-dimension112="594335c2-811f-11f1-86d0-9bb5f4f4297d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">JBL Go 5</a> boasts a wide soundstage packed into a compact, pocketable body. It looks great, sounds even better, and can be customized via a user-friendly companion app. It’s IP68-rated, so it’s highly waterproof, dustproof and drop-proof too.<a class="view-deal button" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow" data-dimension112="594335c2-811f-11f1-86d0-9bb5f4f4297d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">View Deal</a></p></div><div class="product"><a data-dimension112="59433626-811f-11f1-a1ea-058e50cfad1a" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1110px;"><p class="vanilla-image-block" style="padding-top:118.65%;"><img id="dsAxZA3JpSDwfF643VP3eB" name="Micro 3 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/dsAxZA3JpSDwfF643VP3eB.jpg" mos="" align="middle" fullscreen="" width="1110" height="1317" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review" data-dimension112="59433626-811f-11f1-a1ea-058e50cfad1a" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49">Tribit StormBox Micro 3 </a>offers an IP68 rating, a companion app for EQ customization (although some settings cause compression), and simple controls that make the speaker a breeze to use. It has 24 hours of battery life and can even be used as a portable charger. But most importantly, the audio performance really is excellent.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow" data-dimension112="59433626-811f-11f1-a1ea-058e50cfad1a" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49">View Deal</a></p></div><div class="product"><a data-dimension112="5943368a-811f-11f1-82aa-759414816d13" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1425px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="D7QYGUpe98kCsZkVddg4oV" name="Ultimate Ears Wonderboom 4.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/D7QYGUpe98kCsZkVddg4oV.jpg" mos="" align="middle" fullscreen="" width="1425" height="1425" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The mighty and portable Wonderboom 4 is a great outdoor speaker that goes louder than it has any right to. In our <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ue-wonderboom-4-review" data-dimension112="5943368a-811f-11f1-82aa-759414816d13" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">Ultimate Ears WonderBoom 4 review</a>, our reviewer loved its compact size, very good 16-hour battery life and slick rugged looks. It's IP67 water and dust-resistant as well, so you can really take it anywhere to listen to your tunes.<a class="view-deal button" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow" data-dimension112="5943368a-811f-11f1-82aa-759414816d13" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">View Deal</a></p></div><div class="product"><a data-dimension112="4a7e6878-8123-11f1-af3e-05e77453d77d" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$59" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1115px;"><p class="vanilla-image-block" style="padding-top:49.42%;"><img id="U4eB79RfdkjMCrjY6xNuFm" name="EarFun-UBoomL-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/U4eB79RfdkjMCrjY6xNuFm.png" mos="" align="middle" fullscreen="" width="1115" height="551" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/speakers/bluetooth-speakers/earfun-uboom-l-review" data-dimension112="4a7e6878-8123-11f1-af3e-05e77453d77d" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$59">EarFun UBoom L</a> offers big sound on a budget. This portable speaker produces powerful bass, clear vocals and well-balanced sound across a range of genres. With a dedicated outdoor mode and an IP67 resistance rating, the speaker can soundtrack pool parties and hikes. It has great battery life, and the customizable EQ via the user-friendly companion app works really well.<a class="view-deal button" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow" data-dimension112="4a7e6878-8123-11f1-af3e-05e77453d77d" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$59">View Deal</a></p></div><div class="product"><a data-dimension112="594336ee-811f-11f1-8cfc-1d3c74cc3851" data-action="Deal Block" data-label="SoundLink Micro" data-dimension48="SoundLink Micro" data-dimension25="$79" href="https://www.amazon.com/dp/B0FDLCCFQP?" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1105px;"><p class="vanilla-image-block" style="padding-top:103.98%;"><img id="YTZZQubUxh8Cg22rMiKso7" name="Bose soundlink" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/YTZZQubUxh8Cg22rMiKso7.jpg" mos="" align="middle" fullscreen="" width="1105" height="1149" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The SoundLink Micro is a compact, portable speaker you can take on the road. The Bluetooth speaker is waterproof (IPX7 certified) and offers up to 6 hours of play time off a single charge. It can also be paired with another SoundLink speaker to create a stereo mode.<a class="view-deal button" href="https://www.amazon.com/dp/B0FDLCCFQP?" target="_blank" rel="nofollow" data-dimension112="594336ee-811f-11f1-8cfc-1d3c74cc3851" data-action="Deal Block" data-label="SoundLink Micro" data-dimension48="SoundLink Micro" data-dimension25="$79">View Deal</a></p></div><div class="product"><a data-dimension112="594337c0-811f-11f1-8aeb-03772b602a7f" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$83" href="https://www.amazon.com/dp/B09GJVTRNZ?" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="33AJemCSzK8fNYEhkUfBtm" name="JBL Flip 6" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/33AJemCSzK8fNYEhkUfBtm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>JBL just doesn't miss with its Bluetooth speakers. Although the Flip 7 came out earlier this year, the Flip 6 shouldn't be forgotten about. It's now on sale and an awesome deal for this pocket-sized 30W speaker. It's IP67 rated and has a 12 hour battery life, meaning it's perfect to chuck in your backpack for all day outdoor listening. <a class="view-deal button" href="https://www.amazon.com/dp/B09GJVTRNZ?" target="_blank" rel="nofollow" data-dimension112="594337c0-811f-11f1-8aeb-03772b602a7f" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$83">View Deal</a></p></div><div class="product"><a data-dimension112="59433752-811f-11f1-a286-09509732d7c0" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$90" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="3CvgYRTFMbxneUZcepKkae" name="soundcore boom 2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/3CvgYRTFMbxneUZcepKkae.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A speaker of this caliber for just $90 is unheard of. The Boom 2 has a massive 80W power, meaning it will go <em>loud</em>. Like the much pricier JBL options, the Boom 2 has a 24 hour battery life and IP67 rating, so it'll be able to follow you anywhere. <a class="view-deal button" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow" data-dimension112="59433752-811f-11f1-a286-09509732d7c0" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$90">View Deal</a></p></div><div class="product"><a data-dimension112="5943381a-811f-11f1-8333-1faca2219b7a" data-action="Deal Block" data-label="SoundLink Flex (Gen 2)" data-dimension48="SoundLink Flex (Gen 2)" data-dimension25="$99" href="https://www.amazon.com/Bose-SoundLink-Flex-Bluetooth-Speaker/dp/B0FGY8BBVP/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1063px;"><p class="vanilla-image-block" style="padding-top:73.00%;"><img id="4oVcFP6PokTpSnss5yrCLB" name="Bose Soundlink Flex" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/4oVcFP6PokTpSnss5yrCLB.jpg" mos="" align="middle" fullscreen="" width="1063" height="776" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Bose-SoundLink-Flex-Bluetooth-Speaker/dp/B0FGY8BBVP/" target="_blank" rel="nofollow" data-dimension112="5943381a-811f-11f1-8333-1faca2219b7a" data-action="Deal Block" data-label="SoundLink Flex (Gen 2)" data-dimension48="SoundLink Flex (Gen 2)" data-dimension25="$99">SoundLink Flex (Gen 2): was $159 now $99</a></strong><br>The new Bose SoundLink (2nd gen) is a compact and powerful Bluetooth speaker with a durable design. This 1.2-pound speaker is water and dust proof and delivered great sound quality in our <a href="https://www.tomsguide.com/audio/bose-soundlink-flex-2-review" target="_blank">SoundLink Flex gen 2 review</a>. With 12 hours of battery life and $59 off, you have yourself a great deal.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Flex-Bluetooth-Speaker/dp/B0FGY8BBVP/" target="_blank" rel="nofollow" data-dimension112="5943381a-811f-11f1-8333-1faca2219b7a" data-action="Deal Block" data-label="SoundLink Flex (Gen 2)" data-dimension48="SoundLink Flex (Gen 2)" data-dimension25="$99">View Deal</a></p></div><h3 class="article-body__section" id="section-shop-more-deals"><span>Shop more deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Speakers&brands=Sonos%2CAnker%2CBeats&min_discount_ratio=0.95&offer_type=all&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }        .tg-df-container .tg-df-carousel-scroll-left,    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-left { left: 8px; }    .tg-df-container .tg-df-carousel-scroll-right { right: 8px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left { left: 0px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { right: 0px; }    .tg-df-carousel-filters-outer { margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; }    @container tg-df (max-width: 599px) { .tg-df-carousel-filters-outer { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; } }        .tg-df-container .tg-df-carousel-scroll-left:hover,    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right {      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right { right: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-right { right: 0; }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left:hover,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-outer" style="position: relative;">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>        \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3C/div>    \x3C/div>    \x3C/div>      \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>      \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }          const website = params.get('website') || 'tomsguide';          this.website = website;          if (website === 'techradar') {            const squadHeader = this.root.querySelector('.tg-df-savings-squad-header');            if (squadHeader) {               const pic = squadHeader.querySelector('picture');               if (pic) pic.style.display = 'none';            }            const style = document.createElement('style');            style.innerHTML = `              .tg-df-container .hawk-affiliate-link-deal-button, .tg-df-container .tg-df-card-cta { background-color: #5DAF08 !important; }              .tg-df-container .hawk-affiliate-link-deal-button:hover, .tg-df-container .tg-df-card-cta:hover { background-color: #4a8c06 !important; }            `;            this.root.appendChild(style);          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }          this.setupScrollListeners();        }        setupScrollListeners() {          const containers = [             this.root.querySelector('.tg-df-carousel-roundels'),             this.root.querySelector('.tg-df-carousel-filters-wrap'),             this.root.querySelector('#tg-df-grid')          ];                    containers.forEach(container => {             if (!container) return;                          const checkScroll = () => {                if (!container.parentElement) return;                const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left');                const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right');                                if (leftBtn) {                   if (container.scrollLeft <= 5) leftBtn.style.display = 'none';                   else leftBtn.style.display = 'flex';                }                                if (rightBtn) {                   if (container.scrollWidth <= container.clientWidth) {                       rightBtn.style.display = 'none';                   } else if (container.scrollLeft >= container.scrollWidth - container.clientWidth - 5) {                       rightBtn.style.display = 'none';                   } else {                       rightBtn.style.display = 'flex';                   }                }             };                          container.addEventListener('scroll', checkScroll);             checkScroll();                          window.addEventListener('resize', checkScroll);                          const observer = new MutationObserver(checkScroll);             observer.observe(container, { childList: true, subtree: true, characterData: false });          });        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getRowsLimit() {          if (this.rowsSelect && this.rowsSelect.value) return parseInt(this.rowsSelect.value, 10);          let configSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {             configSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {             configSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(configSource);          if (params.has('rows')) return parseInt(params.get('rows'), 10) || 12;          return 12;        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = this.getRowsLimit();          } else {             this.displayLimit += this.getRowsLimit();          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');             const encodedTag = encodeURIComponent(cleanTag);             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    url.searchParams.append('rows', this.getRowsLimit().toString());          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = this.getRowsLimit();          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map((v, idx) => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              const revenueIdVal = generateRevenueId(linkUrl, offerName, merchantName, null);              const rewrittenLinkUrl = rewriteAffiliateLink(linkUrl, area, revenueIdVal);              return `                \x3Ca href="${this.escapeHTML(rewrittenLinkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item"                  data-action="voucher-click"                  data-product-name="${this.escapeHTML(offerName)}"                  data-merchant-name="${this.escapeHTML(merchantName)}"                  data-analytics-id="${this.escapeHTML(offerObj.offer_id || offerObj.id || v.id || '')}"                  data-price=""                  data-previous-price=""                  data-original-link="${this.escapeHTML(linkUrl)}"                  data-revenue-id="${revenueIdVal}"                  data-index="${idx}"                  data-total="${offers.length}"                  data-in-stock="true"                  data-currency="USD"                  data-model-id="${this.escapeHTML(offerObj.model_id || v.model_id || offerObj.id || v.id || '')}"                  data-merchant-id="${this.escapeHTML(offerObj.merchant_id || offerObj.merchant?.id || '')}"                >                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });            // Attach voucher click tracking            const voucherBtns = content.querySelectorAll('[data-action="voucher-click"]');            voucherBtns.forEach(btn => {              btn.addEventListener('click', (e) => {                if (e.target.closest('[data-action="copy-code"]')) return;                                const productName = btn.getAttribute('data-product-name');                const merchantNameAttr = btn.getAttribute('data-merchant-name');                const productId = btn.getAttribute('data-analytics-id');                const price = parseFloat(btn.getAttribute('data-price')) || null;                const prevPriceStr = btn.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = btn.getAttribute('data-original-link');                const rewrittenLink = btn.getAttribute('href');                const revenueId = btn.getAttribute('data-revenue-id');                const index = parseInt(btn.getAttribute('data-index'), 10) || 0;                const inStock = btn.getAttribute('data-in-stock') === 'true';                const totalText = btn.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: btn.getAttribute('data-model-id') || null,                    matchId: btn.getAttribute('data-match-id') || null,                    brand: btn.getAttribute('data-model-brand') || null,                    parent: btn.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: btn.getAttribute('data-merchant-id') || null,                    network: btn.getAttribute('data-merchant-network') || null,                    url: btn.getAttribute('data-merchant-url') || null,                    name: merchantNameAttr                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: btn.getAttribute('data-currency') || 'USD'                };                if (typeof trackDealClick === 'function') {                  trackDealClick(trackingParams);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;          // Inject JSON-LD          try {            let targetNode = this.hostContainer || document.head;            let jsonLdScript = targetNode.querySelector('#tg-df-json-ld-' + this.widgetId);            if (!jsonLdScript) {                jsonLdScript = document.createElement('script');                jsonLdScript.type = 'application/ld+json';                jsonLdScript.id = 'tg-df-json-ld-' + this.widgetId;                targetNode.appendChild(jsonLdScript);            }            const jsonLdData = {              "@context": "https://schema.org",              "@type": "ItemList",              "itemListElement": displayDeals.slice(0, this.displayLimit).map((deal, index) => {                 let isoCurrency = "USD";                 if (deal.currency === '£') isoCurrency = "GBP";                 else if (deal.currency === '€') isoCurrency = "EUR";                 else if (deal.currency === 'A$') isoCurrency = "AUD";                 else if (deal.currency === 'CA$') isoCurrency = "CAD";                 const areaCode = typeof this.getAreaCode === 'function' ? this.getAreaCode() : 'US';                 const revenueId = typeof generateRevenueId === 'function' ? generateRevenueId(deal.url, deal.title, deal.merchant, null) : '';                 const rewrittenLink = typeof rewriteAffiliateLink === 'function' ? rewriteAffiliateLink(deal.url, areaCode, revenueId) : deal.url;                 return {                   "@type": "ListItem",                   "position": index + 1,                   "item": {                     "@type": "Product",                     "name": deal.title,                     "image": deal.image || "",                     "description": deal.description || "",                     "brand": {                       "@type": "Brand",                       "name": deal.brand || ""                     },                     "offers": {                       "@type": "Offer",                       "priceCurrency": isoCurrency,                       "price": deal.rawPrice || 0,                       "url": rewrittenLink,                       "seller": {                         "@type": "Organization",                         "name": deal.merchant || ""                       }                     }                   }                 };              }).filter(item => item.item.name)            };            jsonLdScript.textContent = JSON.stringify(jsonLdData);          } catch(e) { console.warn("JSON-LD generation failed", e); }                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let rightChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             let leftChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-left');             if (this.getViewMode() === 'carousel') {                 // The observer set up in setupScrollListeners handles visibility.                 if (rightChevron) rightChevron.style.display = 'flex';                 if (leftChevron) leftChevron.style.display = 'none'; // reset correctly             } else {                 if (rightChevron) rightChevron.style.display = 'none';                 if (leftChevron) leftChevron.style.display = 'none';             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += this.getRowsLimit();                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');               document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I've tested soundbars twice its price but the 23-driver JBL Bar 1300XMK2 would be my desert island choice ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I've tested $3,000 soundbars. I've tested $67 soundbars. And I've tested pretty much every level in between. At some point, the price-vs-performance ratio must plateau, right? Yes, kind of. Once you're looking at more than around $2,000, I'd forget a soundbar and get either floor-standers, built-ins, or buy each piece individually. You'd probably get a better overall sound. </p><p>But if you want an all-in-one, JBL's finally got the answer for you: the <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300xmk2-review">JBL Bar 1300XMK2</a>. I tested the original last year and loved it, so I was incredibly relieved when the upgraded version blew me away, too. </p><p>The Bar 1300XMK2 has a ton of features that make it one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best all-in-one soundbars</a>, but the painless calibration, fantastic width and height, and ease of use make it unbeatable. Here's why the JBL Bar 1300X MK2 is now my favorite soundbar for a true movie theater experience.</p><div class="product"><a data-dimension112="9d891246-7c6b-11f1-971a-6b5e60a08434" data-action="Deal Block" data-label="Bar 1300XMK2" data-dimension48="Bar 1300XMK2" data-dimension25="$1199.95" href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="2VdHveN77updAKoa8c4okR" name="jbl bar 1300xmk2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/2VdHveN77updAKoa8c4okR.png" mos="" align="middle" fullscreen="" width="2000" height="2000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Not only is the JBL Bar 1300XMK2 one of the best performance-to-price soundbars I've ever tested, it's also $500 off right now. At just $1,200 at time of writing, you're getting even more bang for your buck. The JBL Bar 1300XMK2 has been $1,200 since early June, so I'd expect this new price to hang around for a bit longer.<a class="view-deal button" href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow" data-dimension112="9d891246-7c6b-11f1-971a-6b5e60a08434" data-action="Deal Block" data-label="Bar 1300XMK2" data-dimension48="Bar 1300XMK2" data-dimension25="$1199.95">View Deal</a></p></div><h2 id="home-movie-theater-in-3-2-1">Home movie theater in 3, 2, 1...</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XRGwSE7nSZ3NGj97mMmYtn" name="JBL Bar 1300XMK2 4.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/XRGwSE7nSZ3NGj97mMmYtn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The JBL Bar 1300XMK2 has 23 drivers. Yep. 23 drivers. There are 19 drivers in the soundbar itself, one in each rear, and two 8-inch woofers in the sub. This is an incredible amount of speaker power. You're looking at an 11.1.4 channel configuration. In real terms, this means you've got upfiring sound, side-firing sound, and front-firing sound. And in real, real terms, this means you get truly immersive Dolby Atmos, powerful bass, and crystal-clear dialogue.</p><p>In my 4.5-star review, I wrote, "Every flying scene [in "Top Gun: Maverick"] was particularly outstanding." When Maverick flies in hypersonic, the bass "rumbled through the subwoofer with immense energy and depth." But it wasn't just the low-end: "Each siren blare and alarm was clear over the pounding bass. Equally, the Bar 1300XMK2 balanced high-frequency diegetic sounds like rustling papers and ticking clocks with similarly diegetic dialogue perfectly."</p><p>In short, this is a soundbar to end all soundbars. If I could take one soundbar home forever, it'd be the Bar 1300XMK2. </p><h2 id="power-per-penny-math">Power-per-penny math</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Vd96znQ6EEjdaQVZ2ey3un" name="JBL Bar 1300XMK2 8.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Vd96znQ6EEjdaQVZ2ey3un.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Alright, let's do some soundbar math. Just a forewarning: this is just for fun. This is not supposed to be the pinnacle of what makes a good soundbar. Obviously, your personal experience and personal watching/listening style means more than power-per-price. My intention here is to shine a light on just how powerful the JBL is. </p><p>The JBL Bar 1300XMK2 has a max output of 1570 watts. If we take the MSRP of $1,699 and divide it by the 1570W power, this works out to <strong>$1.08</strong> per watt.  </p><p>Another of my favorites, the Samsung QS750F, costs $899 with 345W, which works out to <strong>$2.60</strong> per watt. </p><p>One of the most high-end soundbars I've tested is the Yamaha True X Surround 90A, which will set you back $3,499 for 630W of power. This works out to <strong>$55.53 </strong>per watt.</p><p>In essence, the JBL Bar 1300XMK2 is a darn powerful soundbar, subwoofer, and rears combo. When I was testing the soundbar, the walls and floorboards were shaking. One of my colleagues came upstairs and asked me what I was doing! </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TcKVby8SNBkZ5mjCbzZpqn" name="JBL Bar 1300XMK2 1.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/TcKVby8SNBkZ5mjCbzZpqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Would you try the JBL Bar 1300XMK2, or would you rather something smaller (and cheaper)? If I had $1,200 to spend on a home theater setup, it would 100% be the 1300XMK2. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eAA6oe"></div>                            </div>                            <script src="https://kwizly.com/embed/eAA6oe.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/airpods/ios-27-beta-3-adds-a-clever-new-control-for-airpods-adaptive-mode"><strong>Apple's iOS 27 beta 3 update finally introduces a brilliantly clever and streamlined control interface for AirPods Adaptive mode, empowering users to instantly fine-tune ambient noise blending intensity with just a simple tap</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review"><strong>The Marshall Acton IV won me over with its charming retro design and incredible sound — and it’s earned a permanent spot on my coffee table</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review"><strong>‘Easy on the eyes and ridiculously stunning sound’ — I tested the Marshall Stanmore IV home speaker and it’s coming for Sonos’ crown</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/ive-tested-soundbars-twice-its-price-but-the-23-driver-jbl-bar-1300xmk2-would-be-my-desert-island-choice</link>
                                                                            <description>
                            <![CDATA[ I tested JBL's new flagship soundbar, and the Bar 1300MK2 might just be the best penny-per-performance option on the market in 2026. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">B4mMZ8VtjkpxAJuPz89jEW</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/WY2xaKuefC6doUxx92r85G-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sun, 12 Jul 2026 09:45:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/WY2xaKuefC6doUxx92r85G-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the jbl bar 1300xmk2 in black photographed against the blue tom&#039;s guide background]]></media:description>                                                            <media:text><![CDATA[the jbl bar 1300xmk2 in black photographed against the blue tom&#039;s guide background]]></media:text>
                                <media:title type="plain"><![CDATA[the jbl bar 1300xmk2 in black photographed against the blue tom&#039;s guide background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/WY2xaKuefC6doUxx92r85G-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I've tested $3,000 soundbars. I've tested $67 soundbars. And I've tested pretty much every level in between. At some point, the price-vs-performance ratio must plateau, right? Yes, kind of. Once you're looking at more than around $2,000, I'd forget a soundbar and get either floor-standers, built-ins, or buy each piece individually. You'd probably get a better overall sound. </p><p>But if you want an all-in-one, JBL's finally got the answer for you: the <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300xmk2-review">JBL Bar 1300XMK2</a>. I tested the original last year and loved it, so I was incredibly relieved when the upgraded version blew me away, too. </p><p>The Bar 1300XMK2 has a ton of features that make it one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best all-in-one soundbars</a>, but the painless calibration, fantastic width and height, and ease of use make it unbeatable. Here's why the JBL Bar 1300X MK2 is now my favorite soundbar for a true movie theater experience.</p><div class="product"><a data-dimension112="9d891246-7c6b-11f1-971a-6b5e60a08434" data-action="Deal Block" data-label="Bar 1300XMK2" data-dimension48="Bar 1300XMK2" data-dimension25="$1199.95" href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="2VdHveN77updAKoa8c4okR" name="jbl bar 1300xmk2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/2VdHveN77updAKoa8c4okR.png" mos="" align="middle" fullscreen="" width="2000" height="2000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Not only is the JBL Bar 1300XMK2 one of the best performance-to-price soundbars I've ever tested, it's also $500 off right now. At just $1,200 at time of writing, you're getting even more bang for your buck. The JBL Bar 1300XMK2 has been $1,200 since early June, so I'd expect this new price to hang around for a bit longer.<a class="view-deal button" href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow" data-dimension112="9d891246-7c6b-11f1-971a-6b5e60a08434" data-action="Deal Block" data-label="Bar 1300XMK2" data-dimension48="Bar 1300XMK2" data-dimension25="$1199.95">View Deal</a></p></div><h2 id="home-movie-theater-in-3-2-1">Home movie theater in 3, 2, 1...</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XRGwSE7nSZ3NGj97mMmYtn" name="JBL Bar 1300XMK2 4.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/XRGwSE7nSZ3NGj97mMmYtn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The JBL Bar 1300XMK2 has 23 drivers. Yep. 23 drivers. There are 19 drivers in the soundbar itself, one in each rear, and two 8-inch woofers in the sub. This is an incredible amount of speaker power. You're looking at an 11.1.4 channel configuration. In real terms, this means you've got upfiring sound, side-firing sound, and front-firing sound. And in real, real terms, this means you get truly immersive Dolby Atmos, powerful bass, and crystal-clear dialogue.</p><p>In my 4.5-star review, I wrote, "Every flying scene [in "Top Gun: Maverick"] was particularly outstanding." When Maverick flies in hypersonic, the bass "rumbled through the subwoofer with immense energy and depth." But it wasn't just the low-end: "Each siren blare and alarm was clear over the pounding bass. Equally, the Bar 1300XMK2 balanced high-frequency diegetic sounds like rustling papers and ticking clocks with similarly diegetic dialogue perfectly."</p><p>In short, this is a soundbar to end all soundbars. If I could take one soundbar home forever, it'd be the Bar 1300XMK2. </p><h2 id="power-per-penny-math">Power-per-penny math</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Vd96znQ6EEjdaQVZ2ey3un" name="JBL Bar 1300XMK2 8.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Vd96znQ6EEjdaQVZ2ey3un.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Alright, let's do some soundbar math. Just a forewarning: this is just for fun. This is not supposed to be the pinnacle of what makes a good soundbar. Obviously, your personal experience and personal watching/listening style means more than power-per-price. My intention here is to shine a light on just how powerful the JBL is. </p><p>The JBL Bar 1300XMK2 has a max output of 1570 watts. If we take the MSRP of $1,699 and divide it by the 1570W power, this works out to <strong>$1.08</strong> per watt.  </p><p>Another of my favorites, the Samsung QS750F, costs $899 with 345W, which works out to <strong>$2.60</strong> per watt. </p><p>One of the most high-end soundbars I've tested is the Yamaha True X Surround 90A, which will set you back $3,499 for 630W of power. This works out to <strong>$55.53 </strong>per watt.</p><p>In essence, the JBL Bar 1300XMK2 is a darn powerful soundbar, subwoofer, and rears combo. When I was testing the soundbar, the walls and floorboards were shaking. One of my colleagues came upstairs and asked me what I was doing! </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TcKVby8SNBkZ5mjCbzZpqn" name="JBL Bar 1300XMK2 1.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/TcKVby8SNBkZ5mjCbzZpqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Would you try the JBL Bar 1300XMK2, or would you rather something smaller (and cheaper)? If I had $1,200 to spend on a home theater setup, it would 100% be the 1300XMK2. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eAA6oe"></div>                            </div>                            <script src="https://kwizly.com/embed/eAA6oe.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/airpods/ios-27-beta-3-adds-a-clever-new-control-for-airpods-adaptive-mode"><strong>Apple's iOS 27 beta 3 update finally introduces a brilliantly clever and streamlined control interface for AirPods Adaptive mode, empowering users to instantly fine-tune ambient noise blending intensity with just a simple tap</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review"><strong>The Marshall Acton IV won me over with its charming retro design and incredible sound — and it’s earned a permanent spot on my coffee table</strong></a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review"><strong>‘Easy on the eyes and ridiculously stunning sound’ — I tested the Marshall Stanmore IV home speaker and it’s coming for Sonos’ crown</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The JBL Xtreme 5 and Marshall Stanmore IV cost the same so which one should you buy? I’ve tested both and here’s what I think ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I test a lot of audio gear, including speakers. Not just <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">Bluetooth speakers</a>, of course: <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">smart speakers</a>, home speakers, you name it. Sometimes, when I test a speaker and notice that it costs the same as another speaker, my mind can't help but pit them against one another. I'm a tech reviewer, after all, that's my job! But what happens when one's a Bluetooth speaker and the other is a mains-powered home speaker?</p><p>That's what happened this week when I reviewed the beautiful <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Marshall Stanmore IV</a>. It's a mains-powered speaker and the latest addition to the fourth iteration of Homeline, Marshall's series of indoors home speakers. I awarded it 4.5-stars and the coveted Editor's Choice Award in my review. And you know what else received the same score?</p><div class="product"><a data-dimension112="3e30f518-7c4b-11f1-8334-7997390bf633" data-action="Deal Block" data-label="Stanmore IV" data-dimension48="Stanmore IV" data-dimension25="$399" href="https://www.marshall.com/us/en/product/stanmore-iv" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1172px;"><p class="vanilla-image-block" style="padding-top:90.61%;"><img id="wPgJyuaU6RscHNoZr9h6gA" name="Marshall Stanmore IV deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wPgJyuaU6RscHNoZr9h6gA.png" mos="" align="middle" fullscreen="" width="1172" height="1062" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Meaty bass with plenty of low-end impact, crisp treble that never sounds fatiguing, and Dynamic Loudness that ensures music sounds the same regardless of the volume: the Stanmore IV does it all. It’s a versatile speaker with Bluetooth, RCA and AUX inputs, and it’s Auracast-enabled for connecting other Marshall home speakers to it.<a class="view-deal button" href="https://www.marshall.com/us/en/product/stanmore-iv" target="_blank" rel="nofollow" data-dimension112="3e30f518-7c4b-11f1-8334-7997390bf633" data-action="Deal Block" data-label="Stanmore IV" data-dimension48="Stanmore IV" data-dimension25="$399">View Deal</a></p></div><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a>. Though I didn't review it — my colleague Erin did — I've listened to it and I agree with everything she said in her review. The Xtreme 5 costs the same as the Stanmore IV: $399 in the U.S. Of course, the Xtreme 5 is a rugged speaker that can be used outdoors, which is quite the opposite of the Stanmore IV. So, if you had to choose one, which is right for you? I know which is right for <em>me</em>, so let's get into it.</p><h2 id="outdoors-or-at-home-listening">Outdoors or at-home listening?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ihaxNtPXv45ssfqDnniGyC" name="Marshall_Stanmore_IV-3" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/ihaxNtPXv45ssfqDnniGyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><div  class="fancy-box"><div class="fancy_box-title">What do they sound like?</div><div class="fancy_box_body"><p class="fancy-box__body-text">If you want to find out how the speakers sound, check out Erin's <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5 review</a> and my <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Marshall Stanmore IV review</a> for our in-depth thoughts on sound quality.</p></div></div><p>The biggest question to ask yourself when deciding between the JBL Xtreme 5 and the Marshall Stanmore IV is this: "Am I taking it outdoors, or leaving it at home?" If the answer is outdoors, then the Xtreme 5 is the natural choice. </p><p>This speaker is extremely rugged, capable of surviving drops and falls. It's IP68-rated, making it waterproof and the most durable speaker on the market right now.</p><p>In terms of portability, the Xtreme 5 weighs 6.4lbs and features a carry strap, so you can lug it around on your shoulder. And just because it's designed for the outdoors doesn't mean you can't use it indoors either. Bluetooth 6.0 wireless and lossless USB-C wired playback make it the speaker that can do everything, really.</p><div class="product"><a data-dimension112="d81786c2-7c4d-11f1-b172-3773e54672d1" data-action="Deal Block" data-label="Xtreme 5" data-dimension48="Xtreme 5" data-dimension25="$399" href="https://www.amazon.com/JBL-Xtreme-Waterproof-Convenient-Built/dp/B0GHYH9DN9/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="tQ9AuHnACGmgriw2w7EMg3" name="jbl xtreme 5 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/tQ9AuHnACGmgriw2w7EMg3.png" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Perfect for every kind of party, be that beach, pool, house, or anything in between, the JBL Xtreme 5 is like the Xtreme 4 on steroids. It’s bigger, heavier, louder, and even has a longer battery life. Our favorite part, though, is the sound quality — it makes you yearn for the dancefloor.<a class="view-deal button" href="https://www.amazon.com/JBL-Xtreme-Waterproof-Convenient-Built/dp/B0GHYH9DN9/" target="_blank" rel="nofollow" data-dimension112="d81786c2-7c4d-11f1-b172-3773e54672d1" data-action="Deal Block" data-label="Xtreme 5" data-dimension48="Xtreme 5" data-dimension25="$399">View Deal</a></p></div><p>But I wouldn't be so quick to write off the Stanmore IV either. Sure, you can't really use it away from a plug but if it's pure power and rock 'n' roll sound you're after, this is <em>the </em>speaker to get. The main advantage of a mains-powered speaker is that it delivers consistently high volume, as the mains power drives the internal amplifiers at full capacity, delivering a great dynamic range. Battery-powered Bluetooth speakers, on the other hand, often try to conserve energy so they can't always play at consistently high volumes.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1316px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="Pxa5kX2RrieVaEeUz3iQ4a" name="JBL_extreme_517.JPG" alt="the JBL Xtreme 5 bluetooth speaker in blue shown with the rugged carry strap and buttons" src="https://cdn.mos.cms.futurecdn.net/v2/t:228,l:236,cw:1316,ch:740,q:80/Pxa5kX2RrieVaEeUz3iQ4a.jpg" mos="" align="middle" fullscreen="" width="1820" height="1024" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Xtreme 5 can get loud, but the Stanmore IV can get <em>very </em>loud. I've soundtracked dinners on my balcony with the Stanmore IV. Even when it's been playing indoors, it's been loud enough to hear outside. Dynamic Loudness, which is Marshall-speak for fine-tuned frequencies that sound the same regardless of the volume, plays a key role here too, as you can listen at the highest volume without any noticeable distortion. Marshall's 'Made of Loud' motto makes a lot of sense now. </p><h2 id="there-is-another">There is another...</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/Ko9LQnFb3AdB8UUG8ruiNG.jpg" alt="A black Marshall Acton IV on a bookshelf" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/6gTtmYbVXuDQbax58Q6bqX.jpg" alt="A JBL Go 5 Bluetooth speaker in blue" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure></figure><div  class="fancy-box"><div class="fancy_box-title">And what do these sound like?</div><div class="fancy_box_body"><p class="fancy-box__body-text">Likewise, to find out more about the sound quality, read my full <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5 review</a> and <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review">Marshall Acton IV review</a>.</p></div></div><p>To quote the great philosopher Yoda, "There is another." The Marshall Stanmore IV isn't the only new kid on the block, or addition to the Marshall Homeline IV, if you will. There's also the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review">Marshall Acton IV</a>, which is basically a smaller (and cheaper) version of the Stanmore IV. It looks near-identical and its internal hardware is almost the same too — except that it has a smaller, 4-inch woofer compared to the Stanmore IV's 5-inch woofer.</p><div class="product"><a data-dimension112="1d34a750-7c51-11f1-a56f-3740612f3da8" data-action="Deal Block" data-label="Acton IV" data-dimension48="Acton IV" data-dimension25="$299" href="https://www.marshall.com/us/en/product/acton-iv" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1182px;"><p class="vanilla-image-block" style="padding-top:74.79%;"><img id="VJtxvHPqLn8d2eE9oxqRfh" name="Screenshot 2026-07-10 at 12.16.43" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/VJtxvHPqLn8d2eE9oxqRfh.png" mos="" align="middle" fullscreen="" width="1182" height="884" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Acton IV improves on its predecessor by introducing RCA input for connecting your turntable, and it’s Auracast-enabled to connect with other Homeline IV speakers, like the Stanmore IV. It boasts room-filling sound, and carries Marshall’s trademark retro looks and amp-like controls. The treble is crisp and vibrant while the bass is meaty and powerful.<a class="view-deal button" href="https://www.marshall.com/us/en/product/acton-iv" target="_blank" rel="nofollow" data-dimension112="1d34a750-7c51-11f1-a56f-3740612f3da8" data-action="Deal Block" data-label="Acton IV" data-dimension48="Acton IV" data-dimension25="$299">View Deal</a></p></div><p>In a similar vein, the JBL Xtreme 5 isn't the only speaker that dropped in March 2026. I reviewed the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> when it came out and I absolutely loved it. It quickly became my go-to for taking outdoors, as I loved its pocketable form factor.</p><div class="product"><a data-dimension112="1d34a822-7c51-11f1-85af-5bf03cf6ee3d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHYJ7BS9/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:82.53%;"><img id="hQdfPTCsudcZKzwvoKBQtU" name="Go-5-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/hQdfPTCsudcZKzwvoKBQtU.jpg" mos="" align="middle" fullscreen="" width="1500" height="1238" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The JBL Go 5 boasts a wide soundstage packed into a compact, pocketable body. It looks great, sounds even better, and can be customized via a user-friendly companion app. It’s IP68-rated, so it’s highly waterproof, dustproof and drop-proof too.<a class="view-deal button" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHYJ7BS9/" target="_blank" rel="nofollow" data-dimension112="1d34a822-7c51-11f1-85af-5bf03cf6ee3d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">View Deal</a></p></div><p>Just like the Xtreme 5, the Go 5 is also IP68-rated and drop-proof. If the Xtreme 5 is out of your budget, the Go 5 is a fantastic sub-$60 speaker. And this is a nice segue into why we're all here: the speaker I'd choose...</p><h2 id="we-have-a-winner">We have a winner</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Z8JfRf6tebrGAR5hWHRh5R" name="Marshall_Stanmore_IV" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/Z8JfRf6tebrGAR5hWHRh5R.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>This is a personal take, but I'm not a fan of carrying around big speakers when I go on a picnic with my partner or friends. Thing is, I'm never going to be blasting my tunes outdoors — <a href="https://www.tomsguide.com/audio/you-can-take-a-portable-bluetooth-speaker-with-you-camping-but-heres-why-its-not-the-best-idea-to-use-it-on-your-hike">I don't like disturbing the local wildlife</a>. All I want is a portable speaker that's loud enough to soundtrack a small gathering without disturbing anyone else.</p><p>The JBL Go 5 does just that — and truth be told, many other small speakers do, too, like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> (which, since reviewing, I still use today). If push came to shove and you asked me to choose between the JBL Xtreme 5 and the Marshall Stanmore IV, I'm going with the Stanmore IV. I love having a speaker that's plugged in all the time, and playing tunes on it when my partner and I are cooking, or she's making art and I'm playing a video game.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="5p8xWyUbUNSFSL4BCdNm8C" name="Marshall_Stanmore_IV-11" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/5p8xWyUbUNSFSL4BCdNm8C.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>If I <em>had </em>to spend $399 on one speaker, the Stanmore IV would be an easy decision for me. It's got the retro looks, the loud and ridiculously stunning sound, and connectivity in spades, and it feels like the perfect home speaker. The Xtreme 5 is no doubt awesome in its own right, but it's just not for me. My ideal combination is a powerful home speaker and a tiny pocketable speaker — so I'm sticking with the Stanmore IV and the Go 5 for as long as I can.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-XjalMO"></div>                            </div>                            <script src="https://kwizly.com/embed/XjalMO.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">The JBL Go 5 is one of the best budget speakers I’ve tested, and it offers stellar sound for just $54</a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">‘Easy on the eyes and ridiculously stunning sound’ — I tested the Marshall Stanmore IV home speaker and it’s coming for Sonos’ crown</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5 review: The most rugged, bassiest, and gnarliest Xtreme yet</a></li></ul><div data-widget-type="multimodelreview" data-model-name="Marshall Stanmore IV,Marshall Acton IV,JBL Go 5,JBL Xtreme 5"></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/should-you-buy-jbl-xtreme-5-marshall-stanmore-iv</link>
                                                                            <description>
                            <![CDATA[ Both the rugged JBL Xtreme 5 and the mains-powered Marshall Stanmore IV cost $399 each, so which one is right for you? Here's the one I'd pick. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">6Byd4bbkJFaR5Rk9tdVpgH</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/PcxoiCDF4i2qc7Y8tnsUd9-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 11 Jul 2026 11:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/PcxoiCDF4i2qc7Y8tnsUd9-1280-80.jpg">
                                                            <media:credit><![CDATA[Erin Bashford / Nikita Achanta / Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[JBL Xtreme 5 and Marshall Stanmore IV]]></media:description>                                                            <media:text><![CDATA[JBL Xtreme 5 and Marshall Stanmore IV]]></media:text>
                                <media:title type="plain"><![CDATA[JBL Xtreme 5 and Marshall Stanmore IV]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/PcxoiCDF4i2qc7Y8tnsUd9-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I test a lot of audio gear, including speakers. Not just <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">Bluetooth speakers</a>, of course: <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">smart speakers</a>, home speakers, you name it. Sometimes, when I test a speaker and notice that it costs the same as another speaker, my mind can't help but pit them against one another. I'm a tech reviewer, after all, that's my job! But what happens when one's a Bluetooth speaker and the other is a mains-powered home speaker?</p><p>That's what happened this week when I reviewed the beautiful <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Marshall Stanmore IV</a>. It's a mains-powered speaker and the latest addition to the fourth iteration of Homeline, Marshall's series of indoors home speakers. I awarded it 4.5-stars and the coveted Editor's Choice Award in my review. And you know what else received the same score?</p><div class="product"><a data-dimension112="3e30f518-7c4b-11f1-8334-7997390bf633" data-action="Deal Block" data-label="Stanmore IV" data-dimension48="Stanmore IV" data-dimension25="$399" href="https://www.marshall.com/us/en/product/stanmore-iv" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1172px;"><p class="vanilla-image-block" style="padding-top:90.61%;"><img id="wPgJyuaU6RscHNoZr9h6gA" name="Marshall Stanmore IV deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wPgJyuaU6RscHNoZr9h6gA.png" mos="" align="middle" fullscreen="" width="1172" height="1062" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Meaty bass with plenty of low-end impact, crisp treble that never sounds fatiguing, and Dynamic Loudness that ensures music sounds the same regardless of the volume: the Stanmore IV does it all. It’s a versatile speaker with Bluetooth, RCA and AUX inputs, and it’s Auracast-enabled for connecting other Marshall home speakers to it.<a class="view-deal button" href="https://www.marshall.com/us/en/product/stanmore-iv" target="_blank" rel="nofollow" data-dimension112="3e30f518-7c4b-11f1-8334-7997390bf633" data-action="Deal Block" data-label="Stanmore IV" data-dimension48="Stanmore IV" data-dimension25="$399">View Deal</a></p></div><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a>. Though I didn't review it — my colleague Erin did — I've listened to it and I agree with everything she said in her review. The Xtreme 5 costs the same as the Stanmore IV: $399 in the U.S. Of course, the Xtreme 5 is a rugged speaker that can be used outdoors, which is quite the opposite of the Stanmore IV. So, if you had to choose one, which is right for you? I know which is right for <em>me</em>, so let's get into it.</p><h2 id="outdoors-or-at-home-listening">Outdoors or at-home listening?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ihaxNtPXv45ssfqDnniGyC" name="Marshall_Stanmore_IV-3" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/ihaxNtPXv45ssfqDnniGyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><div  class="fancy-box"><div class="fancy_box-title">What do they sound like?</div><div class="fancy_box_body"><p class="fancy-box__body-text">If you want to find out how the speakers sound, check out Erin's <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5 review</a> and my <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Marshall Stanmore IV review</a> for our in-depth thoughts on sound quality.</p></div></div><p>The biggest question to ask yourself when deciding between the JBL Xtreme 5 and the Marshall Stanmore IV is this: "Am I taking it outdoors, or leaving it at home?" If the answer is outdoors, then the Xtreme 5 is the natural choice. </p><p>This speaker is extremely rugged, capable of surviving drops and falls. It's IP68-rated, making it waterproof and the most durable speaker on the market right now.</p><p>In terms of portability, the Xtreme 5 weighs 6.4lbs and features a carry strap, so you can lug it around on your shoulder. And just because it's designed for the outdoors doesn't mean you can't use it indoors either. Bluetooth 6.0 wireless and lossless USB-C wired playback make it the speaker that can do everything, really.</p><div class="product"><a data-dimension112="d81786c2-7c4d-11f1-b172-3773e54672d1" data-action="Deal Block" data-label="Xtreme 5" data-dimension48="Xtreme 5" data-dimension25="$399" href="https://www.amazon.com/JBL-Xtreme-Waterproof-Convenient-Built/dp/B0GHYH9DN9/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="tQ9AuHnACGmgriw2w7EMg3" name="jbl xtreme 5 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/tQ9AuHnACGmgriw2w7EMg3.png" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Perfect for every kind of party, be that beach, pool, house, or anything in between, the JBL Xtreme 5 is like the Xtreme 4 on steroids. It’s bigger, heavier, louder, and even has a longer battery life. Our favorite part, though, is the sound quality — it makes you yearn for the dancefloor.<a class="view-deal button" href="https://www.amazon.com/JBL-Xtreme-Waterproof-Convenient-Built/dp/B0GHYH9DN9/" target="_blank" rel="nofollow" data-dimension112="d81786c2-7c4d-11f1-b172-3773e54672d1" data-action="Deal Block" data-label="Xtreme 5" data-dimension48="Xtreme 5" data-dimension25="$399">View Deal</a></p></div><p>But I wouldn't be so quick to write off the Stanmore IV either. Sure, you can't really use it away from a plug but if it's pure power and rock 'n' roll sound you're after, this is <em>the </em>speaker to get. The main advantage of a mains-powered speaker is that it delivers consistently high volume, as the mains power drives the internal amplifiers at full capacity, delivering a great dynamic range. Battery-powered Bluetooth speakers, on the other hand, often try to conserve energy so they can't always play at consistently high volumes.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1316px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="Pxa5kX2RrieVaEeUz3iQ4a" name="JBL_extreme_517.JPG" alt="the JBL Xtreme 5 bluetooth speaker in blue shown with the rugged carry strap and buttons" src="https://cdn.mos.cms.futurecdn.net/v2/t:228,l:236,cw:1316,ch:740,q:80/Pxa5kX2RrieVaEeUz3iQ4a.jpg" mos="" align="middle" fullscreen="" width="1820" height="1024" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Xtreme 5 can get loud, but the Stanmore IV can get <em>very </em>loud. I've soundtracked dinners on my balcony with the Stanmore IV. Even when it's been playing indoors, it's been loud enough to hear outside. Dynamic Loudness, which is Marshall-speak for fine-tuned frequencies that sound the same regardless of the volume, plays a key role here too, as you can listen at the highest volume without any noticeable distortion. Marshall's 'Made of Loud' motto makes a lot of sense now. </p><h2 id="there-is-another">There is another...</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/Ko9LQnFb3AdB8UUG8ruiNG.jpg" alt="A black Marshall Acton IV on a bookshelf" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/6gTtmYbVXuDQbax58Q6bqX.jpg" alt="A JBL Go 5 Bluetooth speaker in blue" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure></figure><div  class="fancy-box"><div class="fancy_box-title">And what do these sound like?</div><div class="fancy_box_body"><p class="fancy-box__body-text">Likewise, to find out more about the sound quality, read my full <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5 review</a> and <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review">Marshall Acton IV review</a>.</p></div></div><p>To quote the great philosopher Yoda, "There is another." The Marshall Stanmore IV isn't the only new kid on the block, or addition to the Marshall Homeline IV, if you will. There's also the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review">Marshall Acton IV</a>, which is basically a smaller (and cheaper) version of the Stanmore IV. It looks near-identical and its internal hardware is almost the same too — except that it has a smaller, 4-inch woofer compared to the Stanmore IV's 5-inch woofer.</p><div class="product"><a data-dimension112="1d34a750-7c51-11f1-a56f-3740612f3da8" data-action="Deal Block" data-label="Acton IV" data-dimension48="Acton IV" data-dimension25="$299" href="https://www.marshall.com/us/en/product/acton-iv" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1182px;"><p class="vanilla-image-block" style="padding-top:74.79%;"><img id="VJtxvHPqLn8d2eE9oxqRfh" name="Screenshot 2026-07-10 at 12.16.43" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/VJtxvHPqLn8d2eE9oxqRfh.png" mos="" align="middle" fullscreen="" width="1182" height="884" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Acton IV improves on its predecessor by introducing RCA input for connecting your turntable, and it’s Auracast-enabled to connect with other Homeline IV speakers, like the Stanmore IV. It boasts room-filling sound, and carries Marshall’s trademark retro looks and amp-like controls. The treble is crisp and vibrant while the bass is meaty and powerful.<a class="view-deal button" href="https://www.marshall.com/us/en/product/acton-iv" target="_blank" rel="nofollow" data-dimension112="1d34a750-7c51-11f1-a56f-3740612f3da8" data-action="Deal Block" data-label="Acton IV" data-dimension48="Acton IV" data-dimension25="$299">View Deal</a></p></div><p>In a similar vein, the JBL Xtreme 5 isn't the only speaker that dropped in March 2026. I reviewed the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> when it came out and I absolutely loved it. It quickly became my go-to for taking outdoors, as I loved its pocketable form factor.</p><div class="product"><a data-dimension112="1d34a822-7c51-11f1-85af-5bf03cf6ee3d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHYJ7BS9/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:82.53%;"><img id="hQdfPTCsudcZKzwvoKBQtU" name="Go-5-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/hQdfPTCsudcZKzwvoKBQtU.jpg" mos="" align="middle" fullscreen="" width="1500" height="1238" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The JBL Go 5 boasts a wide soundstage packed into a compact, pocketable body. It looks great, sounds even better, and can be customized via a user-friendly companion app. It’s IP68-rated, so it’s highly waterproof, dustproof and drop-proof too.<a class="view-deal button" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHYJ7BS9/" target="_blank" rel="nofollow" data-dimension112="1d34a822-7c51-11f1-85af-5bf03cf6ee3d" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">View Deal</a></p></div><p>Just like the Xtreme 5, the Go 5 is also IP68-rated and drop-proof. If the Xtreme 5 is out of your budget, the Go 5 is a fantastic sub-$60 speaker. And this is a nice segue into why we're all here: the speaker I'd choose...</p><h2 id="we-have-a-winner">We have a winner</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Z8JfRf6tebrGAR5hWHRh5R" name="Marshall_Stanmore_IV" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/Z8JfRf6tebrGAR5hWHRh5R.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>This is a personal take, but I'm not a fan of carrying around big speakers when I go on a picnic with my partner or friends. Thing is, I'm never going to be blasting my tunes outdoors — <a href="https://www.tomsguide.com/audio/you-can-take-a-portable-bluetooth-speaker-with-you-camping-but-heres-why-its-not-the-best-idea-to-use-it-on-your-hike">I don't like disturbing the local wildlife</a>. All I want is a portable speaker that's loud enough to soundtrack a small gathering without disturbing anyone else.</p><p>The JBL Go 5 does just that — and truth be told, many other small speakers do, too, like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> (which, since reviewing, I still use today). If push came to shove and you asked me to choose between the JBL Xtreme 5 and the Marshall Stanmore IV, I'm going with the Stanmore IV. I love having a speaker that's plugged in all the time, and playing tunes on it when my partner and I are cooking, or she's making art and I'm playing a video game.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="5p8xWyUbUNSFSL4BCdNm8C" name="Marshall_Stanmore_IV-11" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/5p8xWyUbUNSFSL4BCdNm8C.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>If I <em>had </em>to spend $399 on one speaker, the Stanmore IV would be an easy decision for me. It's got the retro looks, the loud and ridiculously stunning sound, and connectivity in spades, and it feels like the perfect home speaker. The Xtreme 5 is no doubt awesome in its own right, but it's just not for me. My ideal combination is a powerful home speaker and a tiny pocketable speaker — so I'm sticking with the Stanmore IV and the Go 5 for as long as I can.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-XjalMO"></div>                            </div>                            <script src="https://kwizly.com/embed/XjalMO.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">The JBL Go 5 is one of the best budget speakers I’ve tested, and it offers stellar sound for just $54</a></li><li><a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">‘Easy on the eyes and ridiculously stunning sound’ — I tested the Marshall Stanmore IV home speaker and it’s coming for Sonos’ crown</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5 review: The most rugged, bassiest, and gnarliest Xtreme yet</a></li></ul><div data-widget-type="multimodelreview" data-model-name="Marshall Stanmore IV,Marshall Acton IV,JBL Go 5,JBL Xtreme 5"></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The best tech of 2026 (so far): 26 products we've actually tested, rated and loved ]]></title>
                                                                                                <dc:content><![CDATA[ <p>We're now into July, which means we've done six full months of testing here at Tom's Guide in 2026. From <a href="https://www.tomsguide.com/home/coffee-makers/best-coffee-makers">coffee machines</a>, through to vacuums, gaming handhelds, laptops and <a href="https://www.tomsguide.com/best-picks/best-phones">phones</a> — not only is it our job to put tech through its paces, it's what our journalists, editors and contributors love to do. </p><p>I'm Pete, Senior Reviews Editor here at Tom's Guide, and I head up our reviews coverage on site. My team of dedicated reviews writers alone have lab tested and reviewed hundreds of products already this year, with the wider Tom's Guide editorial team having testing dozens more. If you've not had your finger precisely on the pulse of the latest tech this year, well, I don't blame you — it's hard to keep tabs on everything. That's why we exist.</p><p>To help, I've gathered the Tom's Guide team's favorite products from 2026 so far. Obviously, I can't fit everything into a single article, so I asked our subject experts across the site to give me their top picks, too. I've also curated the top five highest scorers, which stand above the rest. Every product on here has been tested by us, hands-on, as part of a full, extensive review.</p><p>Read on to see the tech Tom's Guide loved in the first half of 2026.</p><h3 class="article-body__section" id="section-tom-s-guide-s-top-5"><span>Tom's Guide's Top 5</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QtcQdgVxistWUAqm4jKVz9" name="Samsung-S95H--2" alt="Samsung S95H OLED TV on a desk" src="https://cdn.mos.cms.futurecdn.net/QtcQdgVxistWUAqm4jKVz9.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Every product on this list has scored at least four stars, earning our TG Recommended or Editor's Choice badges. But there are five products on this list that scored our full five stars after testing. These are the best of the best so far this year. To save you some time, here are the links to our reviews.</p><ul><li><strong></strong><a href="https://www.tomsguide.com/cameras-photography/dji-osmo-pocket-4-review"><strong>DJI Osmo Pocket 4 review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/gaming/007-first-light-review"><strong>007 First Light review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/home/kitchenaid-artisan-plus-mixer-review"><strong>KitchenAid Artisan Plus review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/home/kitchen-dining/gozney-dome-xl-2-review"><strong>Gozney Dome XL Gen 2 review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/tvs/oled-tvs/samsung-s95h-oled-tv-review"><strong>Samsung S95H OLED review </strong></a>★★★★★</li></ul><p>Keep reading to see the rest of our top tested picks for 2026.</p><h2 class="article-body__section" id="section-ai"><span>AI</span></h2><h2 id="rokid-glasses"><a href="https://www.tomsguide.com/computing/smart-glasses/rokid-glasses-review">Rokid Glasses</a></h2><p><strong>After 5,000 miles in the Rokids, we were seriously impressed</strong><br><em>Score: </em>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Wnk4WGatZwDH9NWg5qLJd4" name="Rokid Glasses" alt="Rokid Glasses" src="https://cdn.mos.cms.futurecdn.net/Wnk4WGatZwDH9NWg5qLJd4.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The <a href="https://www.tomsguide.com/computing/vr-ar/best-smart-glasses">best smart glasses</a> without a display are cool and all, but their features are usually rather limited to just asking questions about what you see. The Rokid Glasses proved to me that adding a screen turns that superpower into an ambient computing platform — offering features only when you need them and disappearing into the background in some stylish specs. </p><p>With that display, you get real-time conversation translation with text subtitles so you’re not having to try to listen to your smart glasses talk over the person in front of you. And the multi-modality unlocked by asking a question and just getting answers you can see makes them much more subtle. Not to say they’re perfect — the camera quality for snapping pictures suffers in low light and some features are <em>very </em>China-specific (like Alipay). But arm these with Google Gemini in setup, and they’re a genuinely helpful companion on your face for a decent price.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read Jason's full <a href="https://www.tomsguide.com/computing/smart-glasses/rokid-glasses-review">Rokid Glasses review</a></p><div data-model-name="Rokid Glasses,Rokid AI Glasses Style" data-widget-type="peacock" data-widget-title="Today's best Rokid Glasses deals"></div><h2 class="article-body__section" id="section-cameras-drones"><span>Cameras & drones</span></h2><h2 id="gopro-mission-1-pro"><a href="https://www.tomsguide.com/cameras-photography/gopro-action-cameras/gopro-mission-1-pro-review">GoPro Mission 1 Pro</a></h2><p><strong>GoPro's return to form in the action camera game</strong><br><em>Score: </em>★★★★<strong>½</strong></p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="high" data-lazy-src="https://www.youtube-nocookie.com/embed/E2hIrAWkTQo" allowfullscreen></iframe></div></div><p>After years essentially re-releasing the same Hero cameras with minor tweaks, GoPro has finally pushed itself. The Mission 1 Pro is pricey, but it’s the first GoPro I’ve been excited about in a while. It seriously impressed me with its powerful and versatile video credentials in testing, namely open gate 8K and a wealth of shooting features that make it ideal for its target market: professionals. Thanks to revised processor architecture and a shiny new battery, I managed to extract unparalleled battery life and thermals versus other <a href="https://www.tomsguide.com/us/best-action-cameras,review-3046.html">action cameras</a> in my standardized battery tests. GoPro is back!</p><p><em><strong>Pete Wolinski, Senior Reviews & Cameras Editor</strong></em><br>Read Pete’s full <a href="https://www.tomsguide.com/cameras-photography/gopro-action-cameras/gopro-mission-1-pro-review"><u>GoPro Mission 1 Pro review</u></a></p><div data-model-name="GoPro Mission 1 Pro" data-widget-type="peacock"></div><h2 id="dji-osmo-pocket-4"><a href="https://www.tomsguide.com/cameras-photography/dji-osmo-pocket-4-review">DJI Osmo Pocket 4</a></h2><p><strong>The best all-around tool for content creation</strong><br><em>Score: </em>★★★★★</p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="low" data-lazy-src="https://www.youtube-nocookie.com/embed/wBTZhStFd1o" allowfullscreen></iframe></div></div><p>I awarded the DJI Osmo Pocket 4 our full five stars in my review, and that’s because I literally couldn’t find anything wrong with it. Aside from the fact that it’s not available in America (yet?) due to tariffs and DJI’s ongoing feud with the U.S. government. The Osmo Pocket 4 improved on its predecessor in every single way that mattered: video, stabilization, storage, thermals, battery life, and more. If you’re after a single tool for professional-looking content creation, I really can’t think of anything better to recommend.</p><p><em><strong>Pete Wolinski, Senior Reviews & Cameras Editor</strong></em><br>Read Pete’s full <a href="https://www.tomsguide.com/cameras-photography/dji-osmo-pocket-4-review"><u>DJI Osmo Pocket 4 review</u></a></p><div data-model-name="DJI Osmo Pocket 4" data-widget-type="peacock"></div><h2 id="dji-avata-360"><a href="https://www.tomsguide.com/cameras-photography/drones/dji-avata-360-review">DJI Avata 360</a></h2><p><strong>Although a little late to the 360 party, DJI absolutely smashed it</strong><br><em>Score: </em>★★★★<strong>½</strong></p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="low" data-lazy-src="https://www.youtube-nocookie.com/embed/YvK5opkgEow" allowfullscreen></iframe></div></div><p>The DJI Avata 360 does exactly what you want a drone to do: make flying fun. It stakes its claim as the <a href="https://www.tomsguide.com/us/best-drones,review-2412.html">best 360° drone</a> around (granted, there are only two at the time of writing, but still!) and its twin lenses capture every single angle in 8K at 60fps. 120MP hi-res stills, user-friendly editing software, and exceptional build quality make it <em>the </em>drone to buy today. It also undercuts its main competitor, the Antigravity A1, and with dual ways to fly (FPV and with a standard controller), the Avata 360 is versatile and oh-so-powerful. Every day I still think about it — and as someone who’s tested many, many quadcopters, that’s saying a lot.</p><p><em><strong>Nikita Achanta, Senior Reviews Writer</strong></em><br>Read Nikita’s full <a href="https://www.tomsguide.com/cameras-photography/drones/dji-avata-360-review"><u>DJI Avata 360 review</u></a></p><div data-model-name="DJI Avata 360" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-computing-gaming"><span>Computing & gaming</span></h2><h2 id="macbook-neo"><a href="https://www.tomsguide.com/computing/macbooks/apple-macbook-neo-review">MacBook Neo</a></h2><p><strong>Our favorite budget laptop, literally ever</strong><br><em>Score: </em>★★★★<strong>½</strong></p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="low" data-lazy-src="https://www.youtube-nocookie.com/embed/ISmvUpSEG-w" allowfullscreen></iframe></div></div><p>Value for money in laptops has become a bit of a running joke this year (thanks, RAMageddon). But of all the companies building notebooks, I didn’t anticipate it would be Apple to deliver the goods and give you an honest-to-god well-priced system. Yes, there’s been a $100 increase recently, but even with that, the premium aluminum build quality, great display, tactile keyboard and zippy performance of that A18 Pro makes for a great system for the day-to-day essentials. </p><p>I must admit, to see Apple actually having fun with colors in its <a href="https://www.tomsguide.com/best-picks/best-macbook">MacBooks</a> is a refreshing change from the blacks and silvers of its more expensive lineage. All of this comes together into a laptop that has terrified the budget Windows laptop market, and the best ultra portable we’ve tested this year.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read our full <a href="https://www.tomsguide.com/computing/macbooks/apple-macbook-neo-review">MacBook Neo review</a></p><div data-model-name="Apple MacBook Neo" data-widget-type="peacock" data-widget-title="Today's best Apple MacBook Neo deals"></div><h2 id="asus-zenbook-duo-2026"><a href="https://www.tomsguide.com/computing/laptops/asus-zenbook-duo-2026-review">Asus Zenbook Duo (2026)</a></h2><p><strong>Dual OLED panels = chef's kiss</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pzT2YvbKokZUz7jyHGLE59" name="Asus Zenbook Duo (2026)" alt="Asus Zenbook Duo (2026)" src="https://cdn.mos.cms.futurecdn.net/pzT2YvbKokZUz7jyHGLE59.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Yes, this is a dual-screen OLED beauty of a premium laptop; Asus has really paid attention to the design and ergonomics of a system like this. But the real superpower is what lies inside that shell. Intel Core Ultra Series 3 (codenamed Panther Lake) has been a truly generational moment for Team Blue in its <a href="https://www.tomsguide.com/computing/laptops/best-windows-laptops">Windows laptops</a>, and it comes down to huge updates to both the performance and power efficiency. </p><p>Those new CPU cores and efficiency architecture they’re built onto gives this a gargantuan battery life that would make a MacBook blush, while the superpowered GPU (alongside XeSS 3) gives this some serious gaming laptop chops too. Usually, you had to be considerate of just <em>what </em>you wanted to do with your Windows laptop and decide on the strengths and compromises around that. With anything that sports an X9 chip like this, though, that’s no longer a compromise you have to make.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read Jason's full <a href="https://www.tomsguide.com/computing/laptops/asus-zenbook-duo-2026-review">Asus Zenbook Duo (2026) review</a></p><div data-model-name="ASUS ZenBook Duo" data-widget-type="peacock"></div><h2 id="msi-claw-8-ex-ai"><a href="https://www.tomsguide.com/gaming/handheld-gaming/msi-claw-8-ex-ai-plus-review">MSI Claw 8 EX AI+</a></h2><p><strong>A true next-gen gaming handheld</strong><br><em>Score: </em>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vUZrRde9zCurFLDzUsKCZe" name="MSI Claw 8 EX AI+" alt="MSI Claw 8 EX AI+" src="https://cdn.mos.cms.futurecdn.net/vUZrRde9zCurFLDzUsKCZe.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The MSI Claw 8 EX AI+ delivers the performance jump I’ve been waiting years for. With Intel’s new Arc G3 Extreme chip, games are pulling a 20 to 30+ FPS advantage over AMD Z2 Extreme handhelds. Battery life gets a big boost too. I could hit the same performance as the Z2 Extreme at half the power consumption. Combine that with its major redesign bringing more Xbox-like controls, better haptics, and its large 8-inch 16:10 display, and you’re getting, in my opinion, one of the <a href="https://www.tomsguide.com/round-up/best-handheld-gaming-consoles">best handheld gaming consoles</a> of 2026.</p><p><em><strong>Paul Antill, Video Producer</strong></em><br>Read our full <a href="https://www.tomsguide.com/gaming/handheld-gaming/msi-claw-8-ex-ai-plus-review">MSI Claw 8 EX AI+ review</a></p><div data-model-name="MSI Claw 8 EX AI+" data-widget-type="peacock"></div><h2 id="007-first-light"><a href="https://www.tomsguide.com/gaming/007-first-light-review">007 First Light</a></h2><p><strong>Forget GoldenEye: this is the best Bond game ever</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bSj9Dci8XztAKmNPsWEXob" name="007-first-light-screenshot-ps5-pro-08.JPG" alt="007 First Light screenshot taken on PS5 Pro" src="https://cdn.mos.cms.futurecdn.net/bSj9Dci8XztAKmNPsWEXob.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: IO Interactive)</span></figcaption></figure><p>Gamers are being treated to a licensed games renaissance right now. The days of cheap cash-ins launched to coincide with a new blockbuster movie are over. But even in this golden era for video games based on popular media franchises, 007 First Light surpasses expectations. </p><p>Forget GoldenEye, First Light is the best James Bond game ever. With its ludicrously cinematic gameplay, compelling narrative that focuses on a younger Bond (played by Patrick Gibson), and deep commitment to authenticity, 007 First Light is easily the best thing to come out of this long-running spy series in 20 years, movies included. There’s a reason it was only the second video game I’ve given the full five stars. Whether you’re a longtime Bond fan or a newcomer, 007 First Light is quite simply unmissable.</p><p><em><strong>Rory Mellon, Senior Entertainment Editor</strong></em><br>Read Rory's full <a href="https://www.tomsguide.com/gaming/007-first-light-review">007 First Light review</a></p><div data-model-name="007 First Light" data-widget-type="peacock"></div><h2 id="epomaker-glyph"><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></h2><p><strong>Nikita's dream keyboard</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sjAcau9VseSqLieN8EcHke" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/sjAcau9VseSqLieN8EcHke.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ve tested close to 100 <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">keyboards</a> in my time as a reviews writer, and it isn’t often that I get to proclaim, “I’ve found my dream keyboard.” Ever since I tested the Epomaker Glyph, it hasn’t left my desk, and it’s my daily driver when I’m at the office. Typewriter-like aesthetics, a return lever that acts as an Enter or Backspace key, beautiful RGB lighting, a creamy sound profile, and a typing experience that’s nothing short of stunning — all of these characteristics come together to deliver what’s probably the best mechanical keyboard I’ve used for typing. I’m a speed demon when I’m bashing out 109 words per minute of this thing.</p><p><em><strong>Nikita Achanta, Senior Reviews Writer</strong></em><br>Read Nikita’s full <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review"><u>Epomaker Glyph review</u></a></p><p><em>(A note from Nikita's editor, Pete: "I can confirm, the Glyph genuinely hasn't left her desk!")</em></p><div data-model-name="Epomaker Glyph" data-widget-type="peacock"></div><h2 id="msi-mpg-341cqr-qd-oled-x36"><a href="https://www.tomsguide.com/computing/monitors/msi-mpg-341cqr-qd-oled-x36-review">MSI MPG 341CQR QD-OLED X36</a></h2><p><strong>Pro performance for under $1,000</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LDAziKPYjRjFoYgmVRcLK8" name="MSI X36_" alt="An MSI MPG 341CQR QD-OLED X36 gaming monitor in black" src="https://cdn.mos.cms.futurecdn.net/LDAziKPYjRjFoYgmVRcLK8.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It’s no secret that MSI knows how to make a great <a href="https://www.tomsguide.com/us/best-gaming-monitors,review-3114.html">gaming monitor</a>, and its MPG 341CQR QD-OLED X36 is one of the most beautiful displays I’ve ever tested. Blistering 360Hz refresh rate and 0.03ms response time for buttery smooth gameplay? Check. Ridiculously accurate colors and wide color coverage? Yep. AI sleep and wake features that are <em>actually </em>useful and not just a gimmick? Oh yeah. And all of this is packed into a sleek design with plenty of ports. Not much to complain about here, really.</p><p><em><strong>Nikita Achanta, Senior Reviews Writer</strong></em><br>Read Nikita’s full <a href="https://www.tomsguide.com/computing/monitors/msi-mpg-341cqr-qd-oled-x36-review"><u>MSI MPG 341CQR QD-OLED X36 review</u></a></p><div data-model-name="MSI MPG 341CQR QD-OLED X36" data-widget-type="peacock"></div><h2 id="viture-beast-ar-glasses"><a href="https://www.tomsguide.com/computing/smart-glasses/viture-beast-review">Viture Beast AR glasses</a></h2><p><strong>Beast by name, beast by nature</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="iTY7CiVLGP9u3qN25ZR2EW" name="Viture Beast" alt="Viture Beast" src="https://cdn.mos.cms.futurecdn.net/iTY7CiVLGP9u3qN25ZR2EW.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I’ve traveled the world testing all of this year’s biggest AR glasses releases, and Viture Beast stands head and shoulders above the pack as the best I’ve used this year (so far). There’s three reasons why: picture quality, comfort and features. Viture’s color science and fidelity really shines in those micro-OLED displays with a massive 58-degree field of view. There’s a little edge fringing on the sides, but given the massive vision field, it’s unlikely you’ll notice it. And this is packed into a nice-feeling pair of glasses with a soft-touch coating that sits gently on your ears with optimal weight balance. Cap it all off with plenty of customization built in, plus the SpaceWalker app for multi-monitor home office setups, and the Beast are…well…pretty beastly for work and play.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read Jason's full <a href="https://www.tomsguide.com/computing/smart-glasses/viture-beast-review">Viture Beast review</a></p><div data-model-name="Viture Beast" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-fitness"><span>Fitness</span></h2><h2 id="fitbit-air"><a href="https://www.tomsguide.com/wellness/fitness-trackers/fitbit-air-review">Fitbit Air</a></h2><p><strong>The best Whoop alternative, and subscription-free</strong><br><em>Score: </em>★★★★<strong>☆</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4923px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="v4R3RdVkt2tearzZXDp52N" name="FitbitAir-rev-07" alt="Close-up of the Fitbit Air on a person's wrist" src="https://cdn.mos.cms.futurecdn.net/v4R3RdVkt2tearzZXDp52N.jpg" mos="" align="middle" fullscreen="" width="4923" height="2769" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Dan Bracaglia/Tom's Guide)</span></figcaption></figure><p>The Fitbit Air is my fitness tracker of the year so far. Discreet and distraction-free (no screen), this is a fitness tracker that's easy to use, lightweight, and oh-so-comfortable. Just $99 and subscription-free for core features, it offers a week of battery life and covers all the holistic metrics you'd expect from a modern device, like heart health, sleep quality, daily movement, and workouts. For most people, this is the best screen-free fitness tracker you can buy, and the new Google Health companion app makes viewing all your health data a dream.</p><p><em><strong>Jane McGuire, Managing Editor, Fitness</strong></em><br>Read our full <a href="https://www.tomsguide.com/wellness/fitness-trackers/fitbit-air-review">Fitbit Air review</a></p><div data-model-name="Fitbit Air" data-widget-type="peacock"></div><h2 id="salomon-xt-6"><a href="https://www.tomsguide.com/wellness/fitness/salomon-xt-6-review">Salomon XT-6</a></h2><p><strong>Hike, shop or relax in comfort and style</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pz558gVFyYaZd7udisT2c9" name="Salomon XT-6" alt="woman wearing Salomon XT-6 sneakers walking up a steep city hill" src="https://cdn.mos.cms.futurecdn.net/pz558gVFyYaZd7udisT2c9.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / Ashley Thieme)</span></figcaption></figure><p>The Salomon XT-6 shoes have become a staple in my closet. Whether I’m heading to the store or hitting up a challenging trail on the weekend, I know I’ll be supported. I wore these <a href="https://www.tomsguide.com/best-picks/best-hiking-boots">hiking sneakers</a> on a city break, where I clocked over 93,000 steps and didn’t have a single complaint. Not only that, the multidirectional lugs kept me stable on even the trickiest terrain, as I hiked in mud, over rocks, and through water. </p><p>On top of their technical ability, the XT-6s are a fashion statement, worn all over New York by fashion-forward celebs; these shoes have earned a coveted spot in my daily rotation — although you’ll find me cleaning the mud off them after each adventure.</p><p><em><strong>Ashley Thieme, Staff Writer</strong></em><br>Read Ashley’s full <a href="https://www.tomsguide.com/wellness/fitness/salomon-xt-6-review"><u>Salomon XT-6 review</u></a></p><div data-model-name="Salomon XT-6 Trainers" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-homes"><span>Homes</span></h2><h2 id="dyson-v16-piston-animal-cordless"><a href="https://www.tomsguide.com/home/home-appliances/the-dyson-v16-piston-animal-is-a-great-multi-tasker-and-handles-messes-well-but-is-it-worth-the-premium-price">Dyson V16 Piston Animal Cordless</a></h2><p><strong>A powerful cordless Dyson that's perfect for pet owners</strong><br><em>Score: </em>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bdv3dPcZbVhQWhaBEKzSYR" name="Dyson V16 Piston-1-LIST" alt="Dyson V16 Piston Animal" src="https://cdn.mos.cms.futurecdn.net/Bdv3dPcZbVhQWhaBEKzSYR.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I’ve tested my fair share of Dyson <a href="https://www.tomsguide.com/best-picks/best-vacuum-cleaners">vacuum cleaners</a> in my time, but I must say that the V16 combines all of my favorite features in one cleaner. First, its upgraded ‘Fluffy Cones’ head is designed to be used on both carpets and hard flooring without the need to swap over attachments. This is a great time-saver, and lets me quickly zip around my home without pausing. Perhaps my favorite feature is the dust-detecting green light which reveals those hidden dustballs lurking underneath furniture and in corners. Plus, it’s lightweight and easy to store, which is why it’s now my go-to  for daily maintenance, and why I awarded our Editor’s Choice badge.</p><p><em><strong>Cynthia Lawrence, Homes Editor</strong></em><br>Read Cynthia’s full <a href="https://www.tomsguide.com/home/home-appliances/the-dyson-v16-piston-animal-is-a-great-multi-tasker-and-handles-messes-well-but-is-it-worth-the-premium-price"><u>Dyson V16 Piston Animal Cordless review</u></a></p><div data-model-name="Dyson V16 Piston Animal Cordless Vacuum" data-widget-type="peacock"></div><h2 id="kitchenaid-artisan-plus"><a href="https://www.tomsguide.com/home/kitchenaid-artisan-plus-mixer-review">KitchenAid Artisan Plus</a></h2><p><strong>The latest version of KitchenAid's legendary mixer, and the best one yet</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Xa7WjwjzSsdVPrfWLzzEDA" name="KitchenAid Artisan Plus stand mixer review" alt="KitchenAid Artisan Plus stand mixer review" src="https://cdn.mos.cms.futurecdn.net/Xa7WjwjzSsdVPrfWLzzEDA.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future / Camilla Sharman)</span></figcaption></figure><p>I got to test KitchenAid’s newest <a href="https://www.tomsguide.com/best-picks/best-stand-mixers">stand mixer</a>, the Artisan Plus, and it’s even better than I imagined. I’d been happily using KitchenAid’s Artisan stand mixer as my go-to appliance for a good few years, and I really didn’t see how it could be improved, but it has. It may only seem like a few tweaks, but they have made a big difference. From the sliding speed control that just glides, to the in-bowl light, which illuminates the mix — and let's not forget the double-edge flex beater that cleans the bowl as it sweeps past— I loved it so much during testing that I gave it a 5-star rating.</p><p><em><strong>Camilla Sharman, Staff Writer</strong></em><br>Read Camilla’s full <a href="https://www.tomsguide.com/home/kitchenaid-artisan-plus-mixer-review"><u>KitchenAid Artisan Plus review</u></a></p><div data-model-name="KitchenAid Artisan Plus" data-widget-type="peacock"></div><h2 id="gozney-dome-xl-gen-2"><a href="https://www.tomsguide.com/home/kitchen-dining/gozney-dome-xl-2-review">Gozney Dome XL (Gen 2)</a></h2><p><strong>The undisputed king of pizza ovens</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3916px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="Ain7J5FfthYiMBYmVw63sY" name="Gozney Dome 2 pizza oven-16" alt="Gozney Dome XL gen 2" src="https://cdn.mos.cms.futurecdn.net/Ain7J5FfthYiMBYmVw63sY.jpg" mos="" align="middle" fullscreen="" width="3916" height="2203" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>If you’re a dedicated — and I mean dedicated — at-home pizza maker, the Gozney Dome XL (Gen 2) is the closest you’ll come to actually building a <a href="https://www.tomsguide.com/best-picks/best-outdoor-pizza-ovens">pizza oven</a> from scratch. It can run on either gas, wood, or charcoal, has dual temperature probes, and is big enough to fit a 16-inch pie with room to spare. I loved its easy-to-ready displays, and the design of the oven is just gorgeous, too. Yes, it’s huge and it’s expensive (I’ve made equally good pizzas in much cheaper ovens), but this is the oven that’ll signal you’re the real pizzaiolo in your neighborhood. </p><p><em><strong>Mike Prospero, U.S. Editor-in-chief</strong></em><br>Read Mike’s full <a href="https://www.tomsguide.com/home/kitchen-dining/gozney-dome-xl-2-review"><u>Gozney Dome XL (Gen 2) review</u></a></p><div data-model-name="Gozney Dome XL (Gen 2)" data-widget-type="peacock"></div><h2 id="fellow-espresso-series-1"><a href="https://www.tomsguide.com/home/coffee-makers/fellow-espresso-series-1-review">Fellow Espresso Series 1</a></h2><p><strong>Beginners or pros, coffee snobs or otherwise — this one is for everyone</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1980px;"><p class="vanilla-image-block" style="padding-top:56.21%;"><img id="kxPQL4RdbUAPhcdc3CdYdP" name="" alt="Coffee tech from the London Coffee Festival" src="https://cdn.mos.cms.futurecdn.net/kxPQL4RdbUAPhcdc3CdYdP.jpg" mos="" align="middle" fullscreen="" width="1980" height="1113" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When Fellow designed the Espresso Series 1, it looked at the other <a href="https://www.tomsguide.com/home/coffee-makers/best-espresso-machines">espresso machines</a> out there and thought, “I’m going to do something different.” And <em>different</em> the Espresso Series 1 is. When I tested this machine, I loved that it provided coffee-snob-approved features while still being usable for complete beginners. On top of having a litany of customization options on the machine itself, I loved having the app, which walked me through how to make every type of coffee ever invented. You may think this is too ambitious, but for me, the Espresso Series 1 hits all its marks and comes out victorious. </p><p><em><strong>Erin Bashford, Senior Reviews Writer</strong></em><br>Read Erin’s full <a href="https://www.tomsguide.com/home/coffee-makers/fellow-espresso-series-1-review"><u>Fellow Espresso Series 1 review</u></a></p><div data-model-name="Fellow Espresso Series 1" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-phones"><span>Phones</span></h2><h2 id="apple-iphone-17"><a href="https://www.tomsguide.com/phones/iphones/apple-iphone-17-review">Apple iPhone 17</a></h2><p><strong>A surprisingly solid update over the previous generation</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="fXQyeewX6f4cMRqqSucB2D" name="iphone17-1 (2)-2" alt="Sage iPhone 17 in hand" src="https://cdn.mos.cms.futurecdn.net/fXQyeewX6f4cMRqqSucB2D.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Apple launched more new <a href="https://www.tomsguide.com/best-picks/best-phones">phones</a> than in any previous year, but the iPhone 17 remains my favorite for several reasons. One thing that makes it stand out is that it finally got a long-overdue upgrade to a 120Hz refresh rate, making its screen look even more fluid than before in testing. On top of that, I really love how it didn’t get a price hike and still managed to get double the starting storage.</p><p><em><strong>John Velasco, Senior Phones Editor</strong></em><br>Read John's full <a href="https://www.tomsguide.com/phones/iphones/apple-iphone-17-review">iPhone 17 review</a></p><div data-model-name="Apple iPhone 17" data-widget-type="peacock"></div><h2 id="samsung-galaxy-s26-ultra"><a href="https://www.tomsguide.com/phones/samsung-phones/samsung-galaxy-s26-ultra-review">Samsung Galaxy S26 Ultra</a></h2><p><strong>The phone that'll get better over time</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3718px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="XqcuYqJmZMz9675qPoSe9L" name="Galaxy S26 Ultra review-LIST3" alt="Samsung Galaxy S26 Ultra shown in hand" src="https://cdn.mos.cms.futurecdn.net/XqcuYqJmZMz9675qPoSe9L.jpg" mos="" align="middle" fullscreen="" width="3718" height="2091" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When it comes to <a href="https://www.tomsguide.com/us/best-android-phones,review-6051.html">Android phones</a>, nothing caught my eye more than the Galaxy S26 Ultra. Samsung’s flagship received all the iterative updates it deserved, but one particular new feature stood out most: the <a href="https://www.tomsguide.com/phones/samsung-phones/the-galaxy-s26-ultras-privacy-display-is-more-exciting-to-me-than-any-ai-feature">Privacy Display.</a> It works like magic, making the screen look dark when someone tries to peek at it from the side. You can even customize it to block out specific parts of the display — like when you’re messaging someone. This exclusive feature is what sets it apart from everything else.</p><p><em><strong>John Velasco, Senior Phones Editor</strong></em><br>Read our full <a href="https://www.tomsguide.com/phones/samsung-phones/samsung-galaxy-s26-ultra-review">Samsung Galaxy S26 Ultra review</a></p><h2 class="article-body__section" id="section-sleep"><span>Sleep</span></h2><h2 id="saatva-zenhaven-mattress"><a href="https://www.tomsguide.com/mattresses/saatva-zenhaven-latex-mattress-review">Saatva Zenhaven Mattress</a></h2><p><strong>What you should be sleeping on in 2026</strong><br>Score: ★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4000px;"><p class="vanilla-image-block" style="padding-top:56.40%;"><img id="TFabSzYttYMuHXjaezHjp5" name="Saatva Zenhaven Latex mattress in situ in Tom's Guide tester's bedroom." alt="A Saatva Zenhaven Latex mattress in situ in Tom's Guide tester's bedroom." src="https://cdn.mos.cms.futurecdn.net/TFabSzYttYMuHXjaezHjp5.jpg" mos="" align="middle" fullscreen="" width="4000" height="2256" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future/Alex Temblador)</span></figcaption></figure><p>The Saatva Zenhaven latex <a href="https://www.tomsguide.com/uk/mattresses/best-mattress">mattress</a> blew our socks off during testing, thanks to its naturally cooling and supportive sleep feel for eco-conscious shoppers. Handcrafted in the US from (mostly) organic materials, the Zenhaven boasts a flippable design with a different tension on each side: Luxury Plush and Gentle Firm. We appreciated how one side boasted more cushioning for side sleepers, while the unyielding feel of the firmer side best-suited back and stomach sleepers. At $3,549 at full MSRP it’s a splurge, but cheaper during <a href="https://www.tomsguide.com/deals/saatva-mattress-deals"><u>Saatva mattress sales</u></a>. However, it’s worth noting that latex mattresses can last up to 20 years, so we think it’s worth the investment.</p><p><em><strong>Nicola Appleton, Sleep Editor</strong></em><br>Read our full <a href="https://www.tomsguide.com/mattresses/saatva-zenhaven-latex-mattress-review"><u>Saatva Zenhaven Mattress review</u></a></p><div data-model-name="Saatva Zenhaven Latex Mattress" data-widget-type="peacock"></div><h2 id="fitnexa-somnipods-3"><a href="https://www.tomsguide.com/wellness/sleep-tech/fitnexa-somnipods-3-review">Fitnexa SomniPods 3</a></h2><p><strong>If you struggle sleeping, these earbuds are for you</strong><br><strong>Score: </strong>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:748px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="SkZZeo9JyoBnQLVZgqU8FB" name="fitnexa_Crop" alt="Fitnexa Somnipods 3" src="https://cdn.mos.cms.futurecdn.net/v2/t:110,l:226,cw:748,ch:421,q:80/SkZZeo9JyoBnQLVZgqU8FB.jpg" mos="" align="middle" fullscreen="" width="1117" height="628" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Fitnexa SomniPods 3 are compact <a href="https://www.tomsguide.com/wellness/sleep-tech/best-sleep-headphones">sleep earbuds</a> that helped me sleep better in even the most disruptive environments (like my neighbor's son playing basketball at unsociable hours). Fitted with built-in white noise, excellent active noise cancelation, plus sleep tracking features, they’re an easy recommendation for any troubled sleepers. But the real stand-out feature for me is the Fitnexa app. Here, your own AI coach waits for you, ready to provide personalized advice based on your sleep tracking data, activity logs and food diaries. The info isn’t quite on par with a qualified sleep professional but thanks to the wealth of data to draw from, it was pretty spot on in testing, helping take the SomniPods from sleep earbud to all-round sleep enhancer.</p><p><em><strong>Ruth Jones, Senior Sleep Writer</strong></em><br>Read Ruth's full <a href="https://www.tomsguide.com/wellness/sleep-tech/fitnexa-somnipods-3-review"><u>Fitnexa SomniPods 3 review</u></a></p><h2 class="article-body__section" id="section-smart-home-security"><span>Smart home & security</span></h2><h2 id="blink-battery-doorbell-2k"><a href="https://www.tomsguide.com/home/smart-home/the-blink-battery-doorbell-2k-checks-all-of-the-boxes-for-a-budget-friendly-video-doorbell">Blink Battery Doorbell 2K+</a></h2><p><strong>A fantastic way to improve security on a budget</strong><br>Score: ★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.30%;"><img id="yZAvTAEWArFc8txHT9dPfE" name="Blink-Battery-Doorbell-2K+--4" alt="Blink Battery Doorbell 2K+ with app" src="https://cdn.mos.cms.futurecdn.net/yZAvTAEWArFc8txHT9dPfE.jpg" mos="" align="middle" fullscreen="" width="2000" height="1126" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Blink has long been one of our favorite budget smart home brands, and its newest battery-powered <a href="https://www.tomsguide.com/us/best-video-doorbells,review-4468.html">video doorbell</a> furthers its reputation as a value leader. The Battery Doorbell 2K+ delivered very good 2K video in our testing — not the tops, but for a sub-$100 device it was more than acceptable. And, while it runs on AA batteries, they’ll last up to two years on a charge. You can also use this video doorbell without a subscription (you’ll need to purchase a separate adapter), but Blink’s subscription price is relatively affordable. </p><p><em><strong>Mike Prospero, U.S. Editor-in-chief</strong></em><br>Read our full <a href="https://www.tomsguide.com/home/smart-home/the-blink-battery-doorbell-2k-checks-all-of-the-boxes-for-a-budget-friendly-video-doorbell"><u>Blink Battery Doorbell 2K+ review</u></a></p><div data-model-name="Blink Battery Doorbell 2K+" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-tv-audio"><span>TV & Audio</span></h2><h2 id="samsung-s95h-oled"><a href="https://www.tomsguide.com/tvs/oled-tvs/samsung-s95h-oled-tv-review">Samsung S95H OLED</a></h2><p><strong>Supreme picture quality, even in bright rooms</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jqM4wzaAhBUkmZbniDKoy9" name="Samsung-S95H--3" alt="Samsung S95H OLED TV on a desk" src="https://cdn.mos.cms.futurecdn.net/jqM4wzaAhBUkmZbniDKoy9.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ve tested countless <a href="https://www.tomsguide.com/us/best-tvs,review-2224.html">TVs</a>, but the Samsung S95H is the first unit in several years that truly surpassed all my expectations. It completely shatters the familiar rules of OLED, obliterating ambient glare and delivering an unprecedented, jaw-dropping brightness in the self-emissive category. Even the floating metal bezel (a design choice I fully assumed I'd hate) adds a striking, premium presence I grew to love. Between its breathtaking performance and how seamlessly it transitions into a decor piece via Samsung Art Store support, this is a flagship that stands as a definitive standard-bearer.</p><p><em><strong>Kate Kozuch, Managing Editor</strong></em><br>Read Kate's full <a href="https://www.tomsguide.com/tvs/oled-tvs/samsung-s95h-oled-tv-review">Samsung S95H OLED review</a></p><div data-model-name="Samsung S95H" data-widget-type="peacock"></div><h2 id="airpods-max-2"><a href="https://www.tomsguide.com/audio/headphones/apple-airpods-max-2-review">AirPods Max 2</a></h2><p><strong>Everything is dialed up to 11</strong><br>Score: ★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SusawEdjBMCisMriTJVuRF" name="AirpodsMax2_13.JPG" alt="the apple airpods max 2 in blue photographed against a blue tom's guide background, showing the mesh headband, anodized aluminum ear cups, carry case, and controls" src="https://cdn.mos.cms.futurecdn.net/SusawEdjBMCisMriTJVuRF.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It’s impossible to be a tech lover and <em>not</em> have strong opinions about Apple’s new AirPods Max 2. Some people love them, some people hate them, but I’m firmly in the first camp. The AirPods Max 2 are <a href="https://www.tomsguide.com/best-picks/best-over-ear-headphones">headphones</a> that know what they’re about, and they’re not apologizing for it. With gorgeous anodized aluminum ear cups and an all-metal headband, they’re not the lightest cans in the world, but the sound more than makes up for it. </p><p>Every detail is clear, mids are melodic and vivacious, and the bass is balanced, rich, and warm. Of course, you’ve also got industry-leading smart tech features like Live Translation and integrated Siri, but Apple users looking for the best headphones need look no further than Apple’s own.</p><p><em><strong>Erin Bashford, Senior Reviews Writer</strong></em><br>Read Erin’s full <a href="https://www.tomsguide.com/audio/headphones/apple-airpods-max-2-review"><u>Apple AirPods Max 2 review</u></a></p><div data-model-name="Apple AirPods Max 2" data-widget-type="peacock"></div><h2 id="beyerdynamic-amiron-zero"><a href="https://www.tomsguide.com/audio/earbuds/beyerdynamic-amiron-zero-review">Beyerdynamic Amiron ZERO</a></h2><p><strong>Open earbuds that make no sacrifices</strong><br><em>Score</em>: ★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RsTfNvY499tyUtwscUGaza" name="beyerdynamic amiron zero" alt="beyerdynamic amiron zero" src="https://cdn.mos.cms.futurecdn.net/RsTfNvY499tyUtwscUGaza.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Beyerdynamic Amiron ZERO have completely changed the way I think about <a href="https://www.tomsguide.com/us/best-wireless-earbuds,review-6160.html">open-earbuds</a> (and I’ve tested a lot). Up until testing the ZEROs, I’d accepted that if I wanted ambient awareness, comfort and stability while working out, I’d need to sacrifice sound quality — but they prove that’s not the case. They provided me with the detailed listening experience I love from Beyerdynamic’s other headphones, without compromising bass performance, delivering perfectly balanced sound. I wear them when I’m working, running, or hiking in the rain (thanks to IP54 durability). They’re the only open earbuds I’ve tested with no compromises.</p><p><em><strong>Ashley Thieme, Staff Writer</strong></em><br>Read Ashley’s full <a href="https://www.tomsguide.com/audio/earbuds/beyerdynamic-amiron-zero-review"><u>Beyerdynamic Amiron ZERO review</u></a></p><div data-model-name="Beyerdynamic Amiron ZERO" data-widget-type="peacock"></div><h2 id="jbl-xtreme-5"><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a></h2><p><em><strong>The</strong></em><strong> party speaker for summer 2026</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4956px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="MhmJUtGJcUZLnrZejP3SpU" name="Xtreme5-edit" alt="the jbl xtreme 5 with water splashing on it outside in the park showing its waterproofing" src="https://cdn.mos.cms.futurecdn.net/MhmJUtGJcUZLnrZejP3SpU.jpg" mos="" align="middle" fullscreen="" width="4956" height="2788" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Erin Bashford)</span></figcaption></figure><p>The JBL Xtreme 5 came, it saw, and it conquered. JBL really understood the assignment with the surprise follow-up to 2024’s Xtreme 4. Everything about the Xtreme 5 oozes confidence: the deft, controlled bass that’ll have your floorboards shaking and your bones quaking; the literally unbeatable IP68 rating that protects it against rain, sand, and drops; the gorpcore, utilitarian design; and of course the rugged 24-hour battery life. This is <em>the</em> party speaker of 2026. I’ve tested hundreds of audio products and <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">Bluetooth speakers</a>, but the Xtreme 5 is a special one. </p><p><em><strong>Erin Bashford, Senior Reviews Writer</strong></em><br>Read Erin’s full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review"><u>JBL Xtreme 5 review</u></a></p><div data-model-name="JBL Xtreme 5" data-widget-type="peacock"></div><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/reviews">See all Tom's Guide Reviews coverage</a></li><li><a href="https://www.tomsguide.com/savings-squad">Find deals fast with the Tom's Guide Savings Squad</a></li><li><a href="https://www.tomsguide.com/best-picks">Check out all of our Best Picks pages</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/tech/the-best-tech-of-2026-so-far-26-products-weve-actually-tested-rated-and-loved</link>
                                                                            <description>
                            <![CDATA[ Here are our favorite products from 2026 after 6 months of non-stop testing. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">fxer8wYcfWtdzTtoWLRU8V</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/FmXxAvFaaLWwRyAoZLwEyf-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 11 Jul 2026 11:15:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Tech]]></category>
                                                                                                <author><![CDATA[ peter.wolinski@futurenet.com (Peter Wolinski) ]]></author>                    <dc:creator><![CDATA[ Peter Wolinski ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/stgPfXWY7ukw8J8rfC7vjg.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Peter is a Senior Editor at Tom&#039;s Guide, heading up the site&#039;s Reviews team and Cameras section. Having built gaming PCs since he was 10 (that&#039;s a while ago now) he&#039;s a bit of a nerd about components and hardware. He&#039;s also been an iPhone user since the classic iPhone 4, and a Mac user for well over a decade. Experienced in using and testing all kinds of technology — from phones through to tablets, computers, games consoles, cameras and smart home tech — helping people find the best tech for them (at the best prices) is what Peter does best. A photographer since he bought his first camera (a Fujifilm) in 2015, Peter was previously an Editor for Canon-Europe.com. He then edited the Cameras and How To sections of Tom&#039;s Guide. When he&#039;s not crafting helpful, in-depth reviews, Peter can usually be found out and about honing his architectural photography skills, riding his motorcycle around Welsh mountain roads, telling everyone about his two greyhounds, squeezing a few extra FPS out of PC games or perfecting his espresso shots.&lt;/p&gt; ]]></dc:description>
                                                                                                        <dc:contributor><![CDATA[ Nikita Achanta ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Paul Antill ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Jason England ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Ruth Jones ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Nicola Appleton ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ John Velasco ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Kate Kozuch ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Erin Bashford ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Ashley Thieme ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Mike Prospero ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Rory Mellon ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Jane McGuire ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Camilla Sharman ]]></dc:contributor>
                                            <dc:contributor><![CDATA[ Cynthia Lawrence ]]></dc:contributor>
                                                                    <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/FmXxAvFaaLWwRyAoZLwEyf-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A split image showing an iPhone 17, Fitbit Air and Apple AirPods Max 2]]></media:description>                                                            <media:text><![CDATA[A split image showing an iPhone 17, Fitbit Air and Apple AirPods Max 2]]></media:text>
                                <media:title type="plain"><![CDATA[A split image showing an iPhone 17, Fitbit Air and Apple AirPods Max 2]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/FmXxAvFaaLWwRyAoZLwEyf-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>We're now into July, which means we've done six full months of testing here at Tom's Guide in 2026. From <a href="https://www.tomsguide.com/home/coffee-makers/best-coffee-makers">coffee machines</a>, through to vacuums, gaming handhelds, laptops and <a href="https://www.tomsguide.com/best-picks/best-phones">phones</a> — not only is it our job to put tech through its paces, it's what our journalists, editors and contributors love to do. </p><p>I'm Pete, Senior Reviews Editor here at Tom's Guide, and I head up our reviews coverage on site. My team of dedicated reviews writers alone have lab tested and reviewed hundreds of products already this year, with the wider Tom's Guide editorial team having testing dozens more. If you've not had your finger precisely on the pulse of the latest tech this year, well, I don't blame you — it's hard to keep tabs on everything. That's why we exist.</p><p>To help, I've gathered the Tom's Guide team's favorite products from 2026 so far. Obviously, I can't fit everything into a single article, so I asked our subject experts across the site to give me their top picks, too. I've also curated the top five highest scorers, which stand above the rest. Every product on here has been tested by us, hands-on, as part of a full, extensive review.</p><p>Read on to see the tech Tom's Guide loved in the first half of 2026.</p><h3 class="article-body__section" id="section-tom-s-guide-s-top-5"><span>Tom's Guide's Top 5</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QtcQdgVxistWUAqm4jKVz9" name="Samsung-S95H--2" alt="Samsung S95H OLED TV on a desk" src="https://cdn.mos.cms.futurecdn.net/QtcQdgVxistWUAqm4jKVz9.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Every product on this list has scored at least four stars, earning our TG Recommended or Editor's Choice badges. But there are five products on this list that scored our full five stars after testing. These are the best of the best so far this year. To save you some time, here are the links to our reviews.</p><ul><li><strong></strong><a href="https://www.tomsguide.com/cameras-photography/dji-osmo-pocket-4-review"><strong>DJI Osmo Pocket 4 review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/gaming/007-first-light-review"><strong>007 First Light review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/home/kitchenaid-artisan-plus-mixer-review"><strong>KitchenAid Artisan Plus review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/home/kitchen-dining/gozney-dome-xl-2-review"><strong>Gozney Dome XL Gen 2 review </strong></a>★★★★★</li><li><strong></strong><a href="https://www.tomsguide.com/tvs/oled-tvs/samsung-s95h-oled-tv-review"><strong>Samsung S95H OLED review </strong></a>★★★★★</li></ul><p>Keep reading to see the rest of our top tested picks for 2026.</p><h2 class="article-body__section" id="section-ai"><span>AI</span></h2><h2 id="rokid-glasses"><a href="https://www.tomsguide.com/computing/smart-glasses/rokid-glasses-review">Rokid Glasses</a></h2><p><strong>After 5,000 miles in the Rokids, we were seriously impressed</strong><br><em>Score: </em>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Wnk4WGatZwDH9NWg5qLJd4" name="Rokid Glasses" alt="Rokid Glasses" src="https://cdn.mos.cms.futurecdn.net/Wnk4WGatZwDH9NWg5qLJd4.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The <a href="https://www.tomsguide.com/computing/vr-ar/best-smart-glasses">best smart glasses</a> without a display are cool and all, but their features are usually rather limited to just asking questions about what you see. The Rokid Glasses proved to me that adding a screen turns that superpower into an ambient computing platform — offering features only when you need them and disappearing into the background in some stylish specs. </p><p>With that display, you get real-time conversation translation with text subtitles so you’re not having to try to listen to your smart glasses talk over the person in front of you. And the multi-modality unlocked by asking a question and just getting answers you can see makes them much more subtle. Not to say they’re perfect — the camera quality for snapping pictures suffers in low light and some features are <em>very </em>China-specific (like Alipay). But arm these with Google Gemini in setup, and they’re a genuinely helpful companion on your face for a decent price.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read Jason's full <a href="https://www.tomsguide.com/computing/smart-glasses/rokid-glasses-review">Rokid Glasses review</a></p><div data-model-name="Rokid Glasses,Rokid AI Glasses Style" data-widget-type="peacock" data-widget-title="Today's best Rokid Glasses deals"></div><h2 class="article-body__section" id="section-cameras-drones"><span>Cameras & drones</span></h2><h2 id="gopro-mission-1-pro"><a href="https://www.tomsguide.com/cameras-photography/gopro-action-cameras/gopro-mission-1-pro-review">GoPro Mission 1 Pro</a></h2><p><strong>GoPro's return to form in the action camera game</strong><br><em>Score: </em>★★★★<strong>½</strong></p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="high" data-lazy-src="https://www.youtube-nocookie.com/embed/E2hIrAWkTQo" allowfullscreen></iframe></div></div><p>After years essentially re-releasing the same Hero cameras with minor tweaks, GoPro has finally pushed itself. The Mission 1 Pro is pricey, but it’s the first GoPro I’ve been excited about in a while. It seriously impressed me with its powerful and versatile video credentials in testing, namely open gate 8K and a wealth of shooting features that make it ideal for its target market: professionals. Thanks to revised processor architecture and a shiny new battery, I managed to extract unparalleled battery life and thermals versus other <a href="https://www.tomsguide.com/us/best-action-cameras,review-3046.html">action cameras</a> in my standardized battery tests. GoPro is back!</p><p><em><strong>Pete Wolinski, Senior Reviews & Cameras Editor</strong></em><br>Read Pete’s full <a href="https://www.tomsguide.com/cameras-photography/gopro-action-cameras/gopro-mission-1-pro-review"><u>GoPro Mission 1 Pro review</u></a></p><div data-model-name="GoPro Mission 1 Pro" data-widget-type="peacock"></div><h2 id="dji-osmo-pocket-4"><a href="https://www.tomsguide.com/cameras-photography/dji-osmo-pocket-4-review">DJI Osmo Pocket 4</a></h2><p><strong>The best all-around tool for content creation</strong><br><em>Score: </em>★★★★★</p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="low" data-lazy-src="https://www.youtube-nocookie.com/embed/wBTZhStFd1o" allowfullscreen></iframe></div></div><p>I awarded the DJI Osmo Pocket 4 our full five stars in my review, and that’s because I literally couldn’t find anything wrong with it. Aside from the fact that it’s not available in America (yet?) due to tariffs and DJI’s ongoing feud with the U.S. government. The Osmo Pocket 4 improved on its predecessor in every single way that mattered: video, stabilization, storage, thermals, battery life, and more. If you’re after a single tool for professional-looking content creation, I really can’t think of anything better to recommend.</p><p><em><strong>Pete Wolinski, Senior Reviews & Cameras Editor</strong></em><br>Read Pete’s full <a href="https://www.tomsguide.com/cameras-photography/dji-osmo-pocket-4-review"><u>DJI Osmo Pocket 4 review</u></a></p><div data-model-name="DJI Osmo Pocket 4" data-widget-type="peacock"></div><h2 id="dji-avata-360"><a href="https://www.tomsguide.com/cameras-photography/drones/dji-avata-360-review">DJI Avata 360</a></h2><p><strong>Although a little late to the 360 party, DJI absolutely smashed it</strong><br><em>Score: </em>★★★★<strong>½</strong></p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="low" data-lazy-src="https://www.youtube-nocookie.com/embed/YvK5opkgEow" allowfullscreen></iframe></div></div><p>The DJI Avata 360 does exactly what you want a drone to do: make flying fun. It stakes its claim as the <a href="https://www.tomsguide.com/us/best-drones,review-2412.html">best 360° drone</a> around (granted, there are only two at the time of writing, but still!) and its twin lenses capture every single angle in 8K at 60fps. 120MP hi-res stills, user-friendly editing software, and exceptional build quality make it <em>the </em>drone to buy today. It also undercuts its main competitor, the Antigravity A1, and with dual ways to fly (FPV and with a standard controller), the Avata 360 is versatile and oh-so-powerful. Every day I still think about it — and as someone who’s tested many, many quadcopters, that’s saying a lot.</p><p><em><strong>Nikita Achanta, Senior Reviews Writer</strong></em><br>Read Nikita’s full <a href="https://www.tomsguide.com/cameras-photography/drones/dji-avata-360-review"><u>DJI Avata 360 review</u></a></p><div data-model-name="DJI Avata 360" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-computing-gaming"><span>Computing & gaming</span></h2><h2 id="macbook-neo"><a href="https://www.tomsguide.com/computing/macbooks/apple-macbook-neo-review">MacBook Neo</a></h2><p><strong>Our favorite budget laptop, literally ever</strong><br><em>Score: </em>★★★★<strong>½</strong></p><div class="youtube-video" data-nosnippet ><div class="video-aspect-box"><iframe data-lazy-priority="low" data-lazy-src="https://www.youtube-nocookie.com/embed/ISmvUpSEG-w" allowfullscreen></iframe></div></div><p>Value for money in laptops has become a bit of a running joke this year (thanks, RAMageddon). But of all the companies building notebooks, I didn’t anticipate it would be Apple to deliver the goods and give you an honest-to-god well-priced system. Yes, there’s been a $100 increase recently, but even with that, the premium aluminum build quality, great display, tactile keyboard and zippy performance of that A18 Pro makes for a great system for the day-to-day essentials. </p><p>I must admit, to see Apple actually having fun with colors in its <a href="https://www.tomsguide.com/best-picks/best-macbook">MacBooks</a> is a refreshing change from the blacks and silvers of its more expensive lineage. All of this comes together into a laptop that has terrified the budget Windows laptop market, and the best ultra portable we’ve tested this year.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read our full <a href="https://www.tomsguide.com/computing/macbooks/apple-macbook-neo-review">MacBook Neo review</a></p><div data-model-name="Apple MacBook Neo" data-widget-type="peacock" data-widget-title="Today's best Apple MacBook Neo deals"></div><h2 id="asus-zenbook-duo-2026"><a href="https://www.tomsguide.com/computing/laptops/asus-zenbook-duo-2026-review">Asus Zenbook Duo (2026)</a></h2><p><strong>Dual OLED panels = chef's kiss</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pzT2YvbKokZUz7jyHGLE59" name="Asus Zenbook Duo (2026)" alt="Asus Zenbook Duo (2026)" src="https://cdn.mos.cms.futurecdn.net/pzT2YvbKokZUz7jyHGLE59.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Yes, this is a dual-screen OLED beauty of a premium laptop; Asus has really paid attention to the design and ergonomics of a system like this. But the real superpower is what lies inside that shell. Intel Core Ultra Series 3 (codenamed Panther Lake) has been a truly generational moment for Team Blue in its <a href="https://www.tomsguide.com/computing/laptops/best-windows-laptops">Windows laptops</a>, and it comes down to huge updates to both the performance and power efficiency. </p><p>Those new CPU cores and efficiency architecture they’re built onto gives this a gargantuan battery life that would make a MacBook blush, while the superpowered GPU (alongside XeSS 3) gives this some serious gaming laptop chops too. Usually, you had to be considerate of just <em>what </em>you wanted to do with your Windows laptop and decide on the strengths and compromises around that. With anything that sports an X9 chip like this, though, that’s no longer a compromise you have to make.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read Jason's full <a href="https://www.tomsguide.com/computing/laptops/asus-zenbook-duo-2026-review">Asus Zenbook Duo (2026) review</a></p><div data-model-name="ASUS ZenBook Duo" data-widget-type="peacock"></div><h2 id="msi-claw-8-ex-ai"><a href="https://www.tomsguide.com/gaming/handheld-gaming/msi-claw-8-ex-ai-plus-review">MSI Claw 8 EX AI+</a></h2><p><strong>A true next-gen gaming handheld</strong><br><em>Score: </em>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vUZrRde9zCurFLDzUsKCZe" name="MSI Claw 8 EX AI+" alt="MSI Claw 8 EX AI+" src="https://cdn.mos.cms.futurecdn.net/vUZrRde9zCurFLDzUsKCZe.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The MSI Claw 8 EX AI+ delivers the performance jump I’ve been waiting years for. With Intel’s new Arc G3 Extreme chip, games are pulling a 20 to 30+ FPS advantage over AMD Z2 Extreme handhelds. Battery life gets a big boost too. I could hit the same performance as the Z2 Extreme at half the power consumption. Combine that with its major redesign bringing more Xbox-like controls, better haptics, and its large 8-inch 16:10 display, and you’re getting, in my opinion, one of the <a href="https://www.tomsguide.com/round-up/best-handheld-gaming-consoles">best handheld gaming consoles</a> of 2026.</p><p><em><strong>Paul Antill, Video Producer</strong></em><br>Read our full <a href="https://www.tomsguide.com/gaming/handheld-gaming/msi-claw-8-ex-ai-plus-review">MSI Claw 8 EX AI+ review</a></p><div data-model-name="MSI Claw 8 EX AI+" data-widget-type="peacock"></div><h2 id="007-first-light"><a href="https://www.tomsguide.com/gaming/007-first-light-review">007 First Light</a></h2><p><strong>Forget GoldenEye: this is the best Bond game ever</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bSj9Dci8XztAKmNPsWEXob" name="007-first-light-screenshot-ps5-pro-08.JPG" alt="007 First Light screenshot taken on PS5 Pro" src="https://cdn.mos.cms.futurecdn.net/bSj9Dci8XztAKmNPsWEXob.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: IO Interactive)</span></figcaption></figure><p>Gamers are being treated to a licensed games renaissance right now. The days of cheap cash-ins launched to coincide with a new blockbuster movie are over. But even in this golden era for video games based on popular media franchises, 007 First Light surpasses expectations. </p><p>Forget GoldenEye, First Light is the best James Bond game ever. With its ludicrously cinematic gameplay, compelling narrative that focuses on a younger Bond (played by Patrick Gibson), and deep commitment to authenticity, 007 First Light is easily the best thing to come out of this long-running spy series in 20 years, movies included. There’s a reason it was only the second video game I’ve given the full five stars. Whether you’re a longtime Bond fan or a newcomer, 007 First Light is quite simply unmissable.</p><p><em><strong>Rory Mellon, Senior Entertainment Editor</strong></em><br>Read Rory's full <a href="https://www.tomsguide.com/gaming/007-first-light-review">007 First Light review</a></p><div data-model-name="007 First Light" data-widget-type="peacock"></div><h2 id="epomaker-glyph"><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></h2><p><strong>Nikita's dream keyboard</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sjAcau9VseSqLieN8EcHke" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/sjAcau9VseSqLieN8EcHke.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ve tested close to 100 <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">keyboards</a> in my time as a reviews writer, and it isn’t often that I get to proclaim, “I’ve found my dream keyboard.” Ever since I tested the Epomaker Glyph, it hasn’t left my desk, and it’s my daily driver when I’m at the office. Typewriter-like aesthetics, a return lever that acts as an Enter or Backspace key, beautiful RGB lighting, a creamy sound profile, and a typing experience that’s nothing short of stunning — all of these characteristics come together to deliver what’s probably the best mechanical keyboard I’ve used for typing. I’m a speed demon when I’m bashing out 109 words per minute of this thing.</p><p><em><strong>Nikita Achanta, Senior Reviews Writer</strong></em><br>Read Nikita’s full <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review"><u>Epomaker Glyph review</u></a></p><p><em>(A note from Nikita's editor, Pete: "I can confirm, the Glyph genuinely hasn't left her desk!")</em></p><div data-model-name="Epomaker Glyph" data-widget-type="peacock"></div><h2 id="msi-mpg-341cqr-qd-oled-x36"><a href="https://www.tomsguide.com/computing/monitors/msi-mpg-341cqr-qd-oled-x36-review">MSI MPG 341CQR QD-OLED X36</a></h2><p><strong>Pro performance for under $1,000</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LDAziKPYjRjFoYgmVRcLK8" name="MSI X36_" alt="An MSI MPG 341CQR QD-OLED X36 gaming monitor in black" src="https://cdn.mos.cms.futurecdn.net/LDAziKPYjRjFoYgmVRcLK8.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It’s no secret that MSI knows how to make a great <a href="https://www.tomsguide.com/us/best-gaming-monitors,review-3114.html">gaming monitor</a>, and its MPG 341CQR QD-OLED X36 is one of the most beautiful displays I’ve ever tested. Blistering 360Hz refresh rate and 0.03ms response time for buttery smooth gameplay? Check. Ridiculously accurate colors and wide color coverage? Yep. AI sleep and wake features that are <em>actually </em>useful and not just a gimmick? Oh yeah. And all of this is packed into a sleek design with plenty of ports. Not much to complain about here, really.</p><p><em><strong>Nikita Achanta, Senior Reviews Writer</strong></em><br>Read Nikita’s full <a href="https://www.tomsguide.com/computing/monitors/msi-mpg-341cqr-qd-oled-x36-review"><u>MSI MPG 341CQR QD-OLED X36 review</u></a></p><div data-model-name="MSI MPG 341CQR QD-OLED X36" data-widget-type="peacock"></div><h2 id="viture-beast-ar-glasses"><a href="https://www.tomsguide.com/computing/smart-glasses/viture-beast-review">Viture Beast AR glasses</a></h2><p><strong>Beast by name, beast by nature</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="iTY7CiVLGP9u3qN25ZR2EW" name="Viture Beast" alt="Viture Beast" src="https://cdn.mos.cms.futurecdn.net/iTY7CiVLGP9u3qN25ZR2EW.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I’ve traveled the world testing all of this year’s biggest AR glasses releases, and Viture Beast stands head and shoulders above the pack as the best I’ve used this year (so far). There’s three reasons why: picture quality, comfort and features. Viture’s color science and fidelity really shines in those micro-OLED displays with a massive 58-degree field of view. There’s a little edge fringing on the sides, but given the massive vision field, it’s unlikely you’ll notice it. And this is packed into a nice-feeling pair of glasses with a soft-touch coating that sits gently on your ears with optimal weight balance. Cap it all off with plenty of customization built in, plus the SpaceWalker app for multi-monitor home office setups, and the Beast are…well…pretty beastly for work and play.</p><p><em><strong>Jason England, Managing Editor, Core Tech</strong></em><br>Read Jason's full <a href="https://www.tomsguide.com/computing/smart-glasses/viture-beast-review">Viture Beast review</a></p><div data-model-name="Viture Beast" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-fitness"><span>Fitness</span></h2><h2 id="fitbit-air"><a href="https://www.tomsguide.com/wellness/fitness-trackers/fitbit-air-review">Fitbit Air</a></h2><p><strong>The best Whoop alternative, and subscription-free</strong><br><em>Score: </em>★★★★<strong>☆</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4923px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="v4R3RdVkt2tearzZXDp52N" name="FitbitAir-rev-07" alt="Close-up of the Fitbit Air on a person's wrist" src="https://cdn.mos.cms.futurecdn.net/v4R3RdVkt2tearzZXDp52N.jpg" mos="" align="middle" fullscreen="" width="4923" height="2769" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Dan Bracaglia/Tom's Guide)</span></figcaption></figure><p>The Fitbit Air is my fitness tracker of the year so far. Discreet and distraction-free (no screen), this is a fitness tracker that's easy to use, lightweight, and oh-so-comfortable. Just $99 and subscription-free for core features, it offers a week of battery life and covers all the holistic metrics you'd expect from a modern device, like heart health, sleep quality, daily movement, and workouts. For most people, this is the best screen-free fitness tracker you can buy, and the new Google Health companion app makes viewing all your health data a dream.</p><p><em><strong>Jane McGuire, Managing Editor, Fitness</strong></em><br>Read our full <a href="https://www.tomsguide.com/wellness/fitness-trackers/fitbit-air-review">Fitbit Air review</a></p><div data-model-name="Fitbit Air" data-widget-type="peacock"></div><h2 id="salomon-xt-6"><a href="https://www.tomsguide.com/wellness/fitness/salomon-xt-6-review">Salomon XT-6</a></h2><p><strong>Hike, shop or relax in comfort and style</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pz558gVFyYaZd7udisT2c9" name="Salomon XT-6" alt="woman wearing Salomon XT-6 sneakers walking up a steep city hill" src="https://cdn.mos.cms.futurecdn.net/pz558gVFyYaZd7udisT2c9.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / Ashley Thieme)</span></figcaption></figure><p>The Salomon XT-6 shoes have become a staple in my closet. Whether I’m heading to the store or hitting up a challenging trail on the weekend, I know I’ll be supported. I wore these <a href="https://www.tomsguide.com/best-picks/best-hiking-boots">hiking sneakers</a> on a city break, where I clocked over 93,000 steps and didn’t have a single complaint. Not only that, the multidirectional lugs kept me stable on even the trickiest terrain, as I hiked in mud, over rocks, and through water. </p><p>On top of their technical ability, the XT-6s are a fashion statement, worn all over New York by fashion-forward celebs; these shoes have earned a coveted spot in my daily rotation — although you’ll find me cleaning the mud off them after each adventure.</p><p><em><strong>Ashley Thieme, Staff Writer</strong></em><br>Read Ashley’s full <a href="https://www.tomsguide.com/wellness/fitness/salomon-xt-6-review"><u>Salomon XT-6 review</u></a></p><div data-model-name="Salomon XT-6 Trainers" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-homes"><span>Homes</span></h2><h2 id="dyson-v16-piston-animal-cordless"><a href="https://www.tomsguide.com/home/home-appliances/the-dyson-v16-piston-animal-is-a-great-multi-tasker-and-handles-messes-well-but-is-it-worth-the-premium-price">Dyson V16 Piston Animal Cordless</a></h2><p><strong>A powerful cordless Dyson that's perfect for pet owners</strong><br><em>Score: </em>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bdv3dPcZbVhQWhaBEKzSYR" name="Dyson V16 Piston-1-LIST" alt="Dyson V16 Piston Animal" src="https://cdn.mos.cms.futurecdn.net/Bdv3dPcZbVhQWhaBEKzSYR.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I’ve tested my fair share of Dyson <a href="https://www.tomsguide.com/best-picks/best-vacuum-cleaners">vacuum cleaners</a> in my time, but I must say that the V16 combines all of my favorite features in one cleaner. First, its upgraded ‘Fluffy Cones’ head is designed to be used on both carpets and hard flooring without the need to swap over attachments. This is a great time-saver, and lets me quickly zip around my home without pausing. Perhaps my favorite feature is the dust-detecting green light which reveals those hidden dustballs lurking underneath furniture and in corners. Plus, it’s lightweight and easy to store, which is why it’s now my go-to  for daily maintenance, and why I awarded our Editor’s Choice badge.</p><p><em><strong>Cynthia Lawrence, Homes Editor</strong></em><br>Read Cynthia’s full <a href="https://www.tomsguide.com/home/home-appliances/the-dyson-v16-piston-animal-is-a-great-multi-tasker-and-handles-messes-well-but-is-it-worth-the-premium-price"><u>Dyson V16 Piston Animal Cordless review</u></a></p><div data-model-name="Dyson V16 Piston Animal Cordless Vacuum" data-widget-type="peacock"></div><h2 id="kitchenaid-artisan-plus"><a href="https://www.tomsguide.com/home/kitchenaid-artisan-plus-mixer-review">KitchenAid Artisan Plus</a></h2><p><strong>The latest version of KitchenAid's legendary mixer, and the best one yet</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Xa7WjwjzSsdVPrfWLzzEDA" name="KitchenAid Artisan Plus stand mixer review" alt="KitchenAid Artisan Plus stand mixer review" src="https://cdn.mos.cms.futurecdn.net/Xa7WjwjzSsdVPrfWLzzEDA.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future / Camilla Sharman)</span></figcaption></figure><p>I got to test KitchenAid’s newest <a href="https://www.tomsguide.com/best-picks/best-stand-mixers">stand mixer</a>, the Artisan Plus, and it’s even better than I imagined. I’d been happily using KitchenAid’s Artisan stand mixer as my go-to appliance for a good few years, and I really didn’t see how it could be improved, but it has. It may only seem like a few tweaks, but they have made a big difference. From the sliding speed control that just glides, to the in-bowl light, which illuminates the mix — and let's not forget the double-edge flex beater that cleans the bowl as it sweeps past— I loved it so much during testing that I gave it a 5-star rating.</p><p><em><strong>Camilla Sharman, Staff Writer</strong></em><br>Read Camilla’s full <a href="https://www.tomsguide.com/home/kitchenaid-artisan-plus-mixer-review"><u>KitchenAid Artisan Plus review</u></a></p><div data-model-name="KitchenAid Artisan Plus" data-widget-type="peacock"></div><h2 id="gozney-dome-xl-gen-2"><a href="https://www.tomsguide.com/home/kitchen-dining/gozney-dome-xl-2-review">Gozney Dome XL (Gen 2)</a></h2><p><strong>The undisputed king of pizza ovens</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3916px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="Ain7J5FfthYiMBYmVw63sY" name="Gozney Dome 2 pizza oven-16" alt="Gozney Dome XL gen 2" src="https://cdn.mos.cms.futurecdn.net/Ain7J5FfthYiMBYmVw63sY.jpg" mos="" align="middle" fullscreen="" width="3916" height="2203" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>If you’re a dedicated — and I mean dedicated — at-home pizza maker, the Gozney Dome XL (Gen 2) is the closest you’ll come to actually building a <a href="https://www.tomsguide.com/best-picks/best-outdoor-pizza-ovens">pizza oven</a> from scratch. It can run on either gas, wood, or charcoal, has dual temperature probes, and is big enough to fit a 16-inch pie with room to spare. I loved its easy-to-ready displays, and the design of the oven is just gorgeous, too. Yes, it’s huge and it’s expensive (I’ve made equally good pizzas in much cheaper ovens), but this is the oven that’ll signal you’re the real pizzaiolo in your neighborhood. </p><p><em><strong>Mike Prospero, U.S. Editor-in-chief</strong></em><br>Read Mike’s full <a href="https://www.tomsguide.com/home/kitchen-dining/gozney-dome-xl-2-review"><u>Gozney Dome XL (Gen 2) review</u></a></p><div data-model-name="Gozney Dome XL (Gen 2)" data-widget-type="peacock"></div><h2 id="fellow-espresso-series-1"><a href="https://www.tomsguide.com/home/coffee-makers/fellow-espresso-series-1-review">Fellow Espresso Series 1</a></h2><p><strong>Beginners or pros, coffee snobs or otherwise — this one is for everyone</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1980px;"><p class="vanilla-image-block" style="padding-top:56.21%;"><img id="kxPQL4RdbUAPhcdc3CdYdP" name="" alt="Coffee tech from the London Coffee Festival" src="https://cdn.mos.cms.futurecdn.net/kxPQL4RdbUAPhcdc3CdYdP.jpg" mos="" align="middle" fullscreen="" width="1980" height="1113" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When Fellow designed the Espresso Series 1, it looked at the other <a href="https://www.tomsguide.com/home/coffee-makers/best-espresso-machines">espresso machines</a> out there and thought, “I’m going to do something different.” And <em>different</em> the Espresso Series 1 is. When I tested this machine, I loved that it provided coffee-snob-approved features while still being usable for complete beginners. On top of having a litany of customization options on the machine itself, I loved having the app, which walked me through how to make every type of coffee ever invented. You may think this is too ambitious, but for me, the Espresso Series 1 hits all its marks and comes out victorious. </p><p><em><strong>Erin Bashford, Senior Reviews Writer</strong></em><br>Read Erin’s full <a href="https://www.tomsguide.com/home/coffee-makers/fellow-espresso-series-1-review"><u>Fellow Espresso Series 1 review</u></a></p><div data-model-name="Fellow Espresso Series 1" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-phones"><span>Phones</span></h2><h2 id="apple-iphone-17"><a href="https://www.tomsguide.com/phones/iphones/apple-iphone-17-review">Apple iPhone 17</a></h2><p><strong>A surprisingly solid update over the previous generation</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="fXQyeewX6f4cMRqqSucB2D" name="iphone17-1 (2)-2" alt="Sage iPhone 17 in hand" src="https://cdn.mos.cms.futurecdn.net/fXQyeewX6f4cMRqqSucB2D.jpg" mos="" align="middle" fullscreen="" width="3840" height="2160" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Apple launched more new <a href="https://www.tomsguide.com/best-picks/best-phones">phones</a> than in any previous year, but the iPhone 17 remains my favorite for several reasons. One thing that makes it stand out is that it finally got a long-overdue upgrade to a 120Hz refresh rate, making its screen look even more fluid than before in testing. On top of that, I really love how it didn’t get a price hike and still managed to get double the starting storage.</p><p><em><strong>John Velasco, Senior Phones Editor</strong></em><br>Read John's full <a href="https://www.tomsguide.com/phones/iphones/apple-iphone-17-review">iPhone 17 review</a></p><div data-model-name="Apple iPhone 17" data-widget-type="peacock"></div><h2 id="samsung-galaxy-s26-ultra"><a href="https://www.tomsguide.com/phones/samsung-phones/samsung-galaxy-s26-ultra-review">Samsung Galaxy S26 Ultra</a></h2><p><strong>The phone that'll get better over time</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3718px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="XqcuYqJmZMz9675qPoSe9L" name="Galaxy S26 Ultra review-LIST3" alt="Samsung Galaxy S26 Ultra shown in hand" src="https://cdn.mos.cms.futurecdn.net/XqcuYqJmZMz9675qPoSe9L.jpg" mos="" align="middle" fullscreen="" width="3718" height="2091" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When it comes to <a href="https://www.tomsguide.com/us/best-android-phones,review-6051.html">Android phones</a>, nothing caught my eye more than the Galaxy S26 Ultra. Samsung’s flagship received all the iterative updates it deserved, but one particular new feature stood out most: the <a href="https://www.tomsguide.com/phones/samsung-phones/the-galaxy-s26-ultras-privacy-display-is-more-exciting-to-me-than-any-ai-feature">Privacy Display.</a> It works like magic, making the screen look dark when someone tries to peek at it from the side. You can even customize it to block out specific parts of the display — like when you’re messaging someone. This exclusive feature is what sets it apart from everything else.</p><p><em><strong>John Velasco, Senior Phones Editor</strong></em><br>Read our full <a href="https://www.tomsguide.com/phones/samsung-phones/samsung-galaxy-s26-ultra-review">Samsung Galaxy S26 Ultra review</a></p><h2 class="article-body__section" id="section-sleep"><span>Sleep</span></h2><h2 id="saatva-zenhaven-mattress"><a href="https://www.tomsguide.com/mattresses/saatva-zenhaven-latex-mattress-review">Saatva Zenhaven Mattress</a></h2><p><strong>What you should be sleeping on in 2026</strong><br>Score: ★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4000px;"><p class="vanilla-image-block" style="padding-top:56.40%;"><img id="TFabSzYttYMuHXjaezHjp5" name="Saatva Zenhaven Latex mattress in situ in Tom's Guide tester's bedroom." alt="A Saatva Zenhaven Latex mattress in situ in Tom's Guide tester's bedroom." src="https://cdn.mos.cms.futurecdn.net/TFabSzYttYMuHXjaezHjp5.jpg" mos="" align="middle" fullscreen="" width="4000" height="2256" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future/Alex Temblador)</span></figcaption></figure><p>The Saatva Zenhaven latex <a href="https://www.tomsguide.com/uk/mattresses/best-mattress">mattress</a> blew our socks off during testing, thanks to its naturally cooling and supportive sleep feel for eco-conscious shoppers. Handcrafted in the US from (mostly) organic materials, the Zenhaven boasts a flippable design with a different tension on each side: Luxury Plush and Gentle Firm. We appreciated how one side boasted more cushioning for side sleepers, while the unyielding feel of the firmer side best-suited back and stomach sleepers. At $3,549 at full MSRP it’s a splurge, but cheaper during <a href="https://www.tomsguide.com/deals/saatva-mattress-deals"><u>Saatva mattress sales</u></a>. However, it’s worth noting that latex mattresses can last up to 20 years, so we think it’s worth the investment.</p><p><em><strong>Nicola Appleton, Sleep Editor</strong></em><br>Read our full <a href="https://www.tomsguide.com/mattresses/saatva-zenhaven-latex-mattress-review"><u>Saatva Zenhaven Mattress review</u></a></p><div data-model-name="Saatva Zenhaven Latex Mattress" data-widget-type="peacock"></div><h2 id="fitnexa-somnipods-3"><a href="https://www.tomsguide.com/wellness/sleep-tech/fitnexa-somnipods-3-review">Fitnexa SomniPods 3</a></h2><p><strong>If you struggle sleeping, these earbuds are for you</strong><br><strong>Score: </strong>★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:748px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="SkZZeo9JyoBnQLVZgqU8FB" name="fitnexa_Crop" alt="Fitnexa Somnipods 3" src="https://cdn.mos.cms.futurecdn.net/v2/t:110,l:226,cw:748,ch:421,q:80/SkZZeo9JyoBnQLVZgqU8FB.jpg" mos="" align="middle" fullscreen="" width="1117" height="628" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Fitnexa SomniPods 3 are compact <a href="https://www.tomsguide.com/wellness/sleep-tech/best-sleep-headphones">sleep earbuds</a> that helped me sleep better in even the most disruptive environments (like my neighbor's son playing basketball at unsociable hours). Fitted with built-in white noise, excellent active noise cancelation, plus sleep tracking features, they’re an easy recommendation for any troubled sleepers. But the real stand-out feature for me is the Fitnexa app. Here, your own AI coach waits for you, ready to provide personalized advice based on your sleep tracking data, activity logs and food diaries. The info isn’t quite on par with a qualified sleep professional but thanks to the wealth of data to draw from, it was pretty spot on in testing, helping take the SomniPods from sleep earbud to all-round sleep enhancer.</p><p><em><strong>Ruth Jones, Senior Sleep Writer</strong></em><br>Read Ruth's full <a href="https://www.tomsguide.com/wellness/sleep-tech/fitnexa-somnipods-3-review"><u>Fitnexa SomniPods 3 review</u></a></p><h2 class="article-body__section" id="section-smart-home-security"><span>Smart home & security</span></h2><h2 id="blink-battery-doorbell-2k"><a href="https://www.tomsguide.com/home/smart-home/the-blink-battery-doorbell-2k-checks-all-of-the-boxes-for-a-budget-friendly-video-doorbell">Blink Battery Doorbell 2K+</a></h2><p><strong>A fantastic way to improve security on a budget</strong><br>Score: ★★★★☆</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.30%;"><img id="yZAvTAEWArFc8txHT9dPfE" name="Blink-Battery-Doorbell-2K+--4" alt="Blink Battery Doorbell 2K+ with app" src="https://cdn.mos.cms.futurecdn.net/yZAvTAEWArFc8txHT9dPfE.jpg" mos="" align="middle" fullscreen="" width="2000" height="1126" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Blink has long been one of our favorite budget smart home brands, and its newest battery-powered <a href="https://www.tomsguide.com/us/best-video-doorbells,review-4468.html">video doorbell</a> furthers its reputation as a value leader. The Battery Doorbell 2K+ delivered very good 2K video in our testing — not the tops, but for a sub-$100 device it was more than acceptable. And, while it runs on AA batteries, they’ll last up to two years on a charge. You can also use this video doorbell without a subscription (you’ll need to purchase a separate adapter), but Blink’s subscription price is relatively affordable. </p><p><em><strong>Mike Prospero, U.S. Editor-in-chief</strong></em><br>Read our full <a href="https://www.tomsguide.com/home/smart-home/the-blink-battery-doorbell-2k-checks-all-of-the-boxes-for-a-budget-friendly-video-doorbell"><u>Blink Battery Doorbell 2K+ review</u></a></p><div data-model-name="Blink Battery Doorbell 2K+" data-widget-type="peacock"></div><h2 class="article-body__section" id="section-tv-audio"><span>TV & Audio</span></h2><h2 id="samsung-s95h-oled"><a href="https://www.tomsguide.com/tvs/oled-tvs/samsung-s95h-oled-tv-review">Samsung S95H OLED</a></h2><p><strong>Supreme picture quality, even in bright rooms</strong><br><em>Score: </em>★★★★★</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jqM4wzaAhBUkmZbniDKoy9" name="Samsung-S95H--3" alt="Samsung S95H OLED TV on a desk" src="https://cdn.mos.cms.futurecdn.net/jqM4wzaAhBUkmZbniDKoy9.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ve tested countless <a href="https://www.tomsguide.com/us/best-tvs,review-2224.html">TVs</a>, but the Samsung S95H is the first unit in several years that truly surpassed all my expectations. It completely shatters the familiar rules of OLED, obliterating ambient glare and delivering an unprecedented, jaw-dropping brightness in the self-emissive category. Even the floating metal bezel (a design choice I fully assumed I'd hate) adds a striking, premium presence I grew to love. Between its breathtaking performance and how seamlessly it transitions into a decor piece via Samsung Art Store support, this is a flagship that stands as a definitive standard-bearer.</p><p><em><strong>Kate Kozuch, Managing Editor</strong></em><br>Read Kate's full <a href="https://www.tomsguide.com/tvs/oled-tvs/samsung-s95h-oled-tv-review">Samsung S95H OLED review</a></p><div data-model-name="Samsung S95H" data-widget-type="peacock"></div><h2 id="airpods-max-2"><a href="https://www.tomsguide.com/audio/headphones/apple-airpods-max-2-review">AirPods Max 2</a></h2><p><strong>Everything is dialed up to 11</strong><br>Score: ★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SusawEdjBMCisMriTJVuRF" name="AirpodsMax2_13.JPG" alt="the apple airpods max 2 in blue photographed against a blue tom's guide background, showing the mesh headband, anodized aluminum ear cups, carry case, and controls" src="https://cdn.mos.cms.futurecdn.net/SusawEdjBMCisMriTJVuRF.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It’s impossible to be a tech lover and <em>not</em> have strong opinions about Apple’s new AirPods Max 2. Some people love them, some people hate them, but I’m firmly in the first camp. The AirPods Max 2 are <a href="https://www.tomsguide.com/best-picks/best-over-ear-headphones">headphones</a> that know what they’re about, and they’re not apologizing for it. With gorgeous anodized aluminum ear cups and an all-metal headband, they’re not the lightest cans in the world, but the sound more than makes up for it. </p><p>Every detail is clear, mids are melodic and vivacious, and the bass is balanced, rich, and warm. Of course, you’ve also got industry-leading smart tech features like Live Translation and integrated Siri, but Apple users looking for the best headphones need look no further than Apple’s own.</p><p><em><strong>Erin Bashford, Senior Reviews Writer</strong></em><br>Read Erin’s full <a href="https://www.tomsguide.com/audio/headphones/apple-airpods-max-2-review"><u>Apple AirPods Max 2 review</u></a></p><div data-model-name="Apple AirPods Max 2" data-widget-type="peacock"></div><h2 id="beyerdynamic-amiron-zero"><a href="https://www.tomsguide.com/audio/earbuds/beyerdynamic-amiron-zero-review">Beyerdynamic Amiron ZERO</a></h2><p><strong>Open earbuds that make no sacrifices</strong><br><em>Score</em>: ★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RsTfNvY499tyUtwscUGaza" name="beyerdynamic amiron zero" alt="beyerdynamic amiron zero" src="https://cdn.mos.cms.futurecdn.net/RsTfNvY499tyUtwscUGaza.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Beyerdynamic Amiron ZERO have completely changed the way I think about <a href="https://www.tomsguide.com/us/best-wireless-earbuds,review-6160.html">open-earbuds</a> (and I’ve tested a lot). Up until testing the ZEROs, I’d accepted that if I wanted ambient awareness, comfort and stability while working out, I’d need to sacrifice sound quality — but they prove that’s not the case. They provided me with the detailed listening experience I love from Beyerdynamic’s other headphones, without compromising bass performance, delivering perfectly balanced sound. I wear them when I’m working, running, or hiking in the rain (thanks to IP54 durability). They’re the only open earbuds I’ve tested with no compromises.</p><p><em><strong>Ashley Thieme, Staff Writer</strong></em><br>Read Ashley’s full <a href="https://www.tomsguide.com/audio/earbuds/beyerdynamic-amiron-zero-review"><u>Beyerdynamic Amiron ZERO review</u></a></p><div data-model-name="Beyerdynamic Amiron ZERO" data-widget-type="peacock"></div><h2 id="jbl-xtreme-5"><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a></h2><p><em><strong>The</strong></em><strong> party speaker for summer 2026</strong><br><em>Score: </em>★★★★<strong>½</strong></p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4956px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="MhmJUtGJcUZLnrZejP3SpU" name="Xtreme5-edit" alt="the jbl xtreme 5 with water splashing on it outside in the park showing its waterproofing" src="https://cdn.mos.cms.futurecdn.net/MhmJUtGJcUZLnrZejP3SpU.jpg" mos="" align="middle" fullscreen="" width="4956" height="2788" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Erin Bashford)</span></figcaption></figure><p>The JBL Xtreme 5 came, it saw, and it conquered. JBL really understood the assignment with the surprise follow-up to 2024’s Xtreme 4. Everything about the Xtreme 5 oozes confidence: the deft, controlled bass that’ll have your floorboards shaking and your bones quaking; the literally unbeatable IP68 rating that protects it against rain, sand, and drops; the gorpcore, utilitarian design; and of course the rugged 24-hour battery life. This is <em>the</em> party speaker of 2026. I’ve tested hundreds of audio products and <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">Bluetooth speakers</a>, but the Xtreme 5 is a special one. </p><p><em><strong>Erin Bashford, Senior Reviews Writer</strong></em><br>Read Erin’s full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review"><u>JBL Xtreme 5 review</u></a></p><div data-model-name="JBL Xtreme 5" data-widget-type="peacock"></div><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/reviews">See all Tom's Guide Reviews coverage</a></li><li><a href="https://www.tomsguide.com/savings-squad">Find deals fast with the Tom's Guide Savings Squad</a></li><li><a href="https://www.tomsguide.com/best-picks">Check out all of our Best Picks pages</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tested JBL’s newest flagship soundbar — and it’s proof you don’t need to spend $2k on a full home theater setup ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The JBL Bar 1300XMK2 has a lot to live up to. Its predecessor completely blew me away with its movie-theater-accurate sound and unbeatable ease of use. So is the MK2 enough to be one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a>? </p><p>If you couldn’t tell from the 4.5-star rating, yes. Yes it is. This is the easiest (and, arguably, the most affordable) way to achieve cinematic Dolby Atmos at home. Dialogue is crystal-clear, even in bombastic action scenes, and high-frequency detail sounds aren’t swallowed by bass. The sound is just unbeatable at this price. </p><p>At $1,699, this isn’t a cheap product by any means. But if you know anything about home theater, you’ll know that this <em>actually</em> isn’t too expensive for an 11.1.4 channel configuration — with 25 individual drivers. Want to find out more? Keep reading this JBL Bar 1300XMK2 review. </p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-specs"><span>JBL Bar 1300XMK2 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow">$1,699</a> / <a href="https://www.amazon.co.uk/JBL-11-1-4-Channel-Bluetooth-Detachable-Subwoofer/dp/B0FH2RPQVB" target="_blank" rel="nofollow">£1,299</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI, Bluetooth, Wi-Fi, USB, Optical</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>Soundbar: 13 pounds / Subwoofer: 26 pounds / Rears: 2.8 pounds each</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>Soundbar: 40.6" x 2.3" x 5.4" / Subwoofer: 12.4” x 10.9” x 10.8” / Rears: 8" x 2.3" x 5.4"</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>11.1.4</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-price-availability"><span>JBL Bar 1300XMK2 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ksgHdzPCWF3QxG7kfhymqn" name="JBL Bar 1300XMK2 6.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/ksgHdzPCWF3QxG7kfhymqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The JBL Bar 13000XMK2 is <a href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow">$1,699 from Amazon U.S.</a> In the U.K., you’re looking at an MSRP of £1,299, but it’s currently “Coming Soon” on JBL’s site. Strangely, though, you can grab it from <a href="https://www.amazon.co.uk/JBL-11-1-4-Channel-Bluetooth-Detachable-Subwoofer/dp/B0FH2RPQVB" target="_blank" rel="nofollow">Amazon U.K. for around £1,160</a>.</p><p>Although this four-digit MSRP <em>looks</em> expensive, I promise you, it’s worth every penny and is actually cheaper than similarly spec’d soundbars. The <em>power</em> in this speaker is almost unparalleled. Of all the soundbars I’ve tested, only the $900 <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> has a better performance-per-penny ratio. </p><p>I’ve also tested the <a href="https://www.tomsguide.com/audio/soundbars/kef-xio-review">KEF XIO</a> and the <a href="https://www.tomsguide.com/audio/soundbars/yamaha-true-x-surround-90a-review">Yamaha True X Surround 90A </a>($3,499), both incredible soundbars in their own right, but both will set you back considerably more than the JBL — without a sub and rears, too. </p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-design"><span>JBL Bar 1300XMK2 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uQDNtcvzR6tMJZuy2Zyvqn" name="JBL Bar 1300XMK2 2.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/uQDNtcvzR6tMJZuy2Zyvqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In a word, the JBL Bar 1300XMK2 is powerful. This soundbar was rattling my floorboards, vibrating my skeleton, shaking my walls. With a whopping 2470W of power, this is a killer sound system. Thankfully, the soundbar also knows where to <em>put </em>that power. </p><p>There are a whopping 23 drivers in this 11.1.4 channel setup. The subwoofer itself has two 8-inch drivers, on top of the 19 drivers in the soundbar itself. In real terms, this means the 1300XMK2 is a behemoth, with sound worthy of a movie theater.</p><p>In terms of looks, the 1300XMK2 is your average soundbar. It’s not particularly gorgeous or particularly revolting. The bar itself is 40 inches long, so I’d recommend using it only if you have a 65” TV and up, otherwise it might look strange. </p><p>The subwoofer is <em>huge</em> and a frankly massive 26 pounds, so, again, only get this if you have space. </p><h2 id="controls-8">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="u9eBdprQrXoC4mK2dCGLZn" name="JBL_.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/u9eBdprQrXoC4mK2dCGLZn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>You can control the JBL Bar 1300XMK2 via either the remote or your phone. The remote is long and lightweight, with pleasantly tactile buttons. It’s much bigger than Bose’s soundbar remote, but feels more plasticky than Samsung’s. </p><p>Unlike the KEF XIO, you can control the soundbar settings while watching TV. You can adjust subwoofer volume and surround volume directly from your phone. I found this responded instantly with zero stuttering. </p><p>The on-bar buttons are minimal: just volume up and down. I don’t miss the buttons on the bar — I only really ever use my phone and remote anyway. </p><h2 id="features-5">Features</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TcKVby8SNBkZ5mjCbzZpqn" name="JBL Bar 1300XMK2 1.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/TcKVby8SNBkZ5mjCbzZpqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ll discuss the app features in detail just below, but the Bar 1300XMK2 has a huge range of features. There’s automatic room calibration, broadcast mode, EQ adjustment, night mode, PureVoice mode, and smart mode. </p><p>One feature is missing, though: 4K / 120Hz pass-through for gaming. The Bar 1300XMK2's pass-through HDMI port is limited to 60Hz, so if you wanted to run your console through the soundbar (if you’re short on HDMI ports), unfortunately, you won’t be able to play at a 120fps frame rate in 4K. Not great news for FPS or other fast-paced games.</p><p>In fairness, this feature isn't ubiquitous with soundbars, and it'll only be an issue if you have no HDMI 2.1 port free on your TV. But this was a complaint we had about the previous model, so it's disappointing to see no improvement here on the new one.</p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-app"><span>JBL Bar 1300XMK2 review: App</span></h2><p>I’ll go through all the main features one by one: room calibration is exactly as it sounds. The soundbar sends out sonic pulses to the rears, which helps position the sound waves for 360° audio. This was the first thing I did when I unboxed the soundbar and it made my movies sound super immersive. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yVPWZ85RHnqGWcYgbNntGR" name="jbl bar 1300 x screenshots" alt="the JBL Bar 1300XMK2 app connectivity showing features and room calibration of the soundbar and subwoofer and surrounds" src="https://cdn.mos.cms.futurecdn.net/yVPWZ85RHnqGWcYgbNntGR.png" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / JBL)</span></figcaption></figure><p>As you can see, the JBL One app is well-designed and visually appealing, and I experienced no connectivity issues. </p><p>Next: broadcast mode. This enables you to keep the surround speakers connected to the soundbar even when you’re in different rooms. So, in theory, you could listen to a movie playing in the living room while you’re in the bathtub. Not sure why you’d want to, but you can. </p><p>You can also connect your phone to the surrounds and use them as individual speakers, and they have Auracast. In theory, you can link them up to JBL speakers like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">Xtreme 5</a>.</p><p>Of course, night mode and EQ adjustments are exactly what you might think — night mode reduces bass for fewer subwoofer-related sleep interruptions, and EQ allows you to completely customize the soundbar’s output. </p><p>Smart mode is interesting — it’s effectively a “standard” “Music” and “Movie” mode you can toggle to come on automatically or manually. Smart mode adjusts the sound mode based on the input. For example, if it detects a DVD player, it will activate movie mode, and if you play via a <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">music streaming service</a>, it will activate music. </p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-sound-quality"><span>JBL Bar 1300XMK2 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="86pjqE4NiTp36zWkQ5hfYn" name="JBL Bar 1300XMK2 5.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/86pjqE4NiTp36zWkQ5hfYn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I test every soundbar with “Top Gun: Maverick” on 4K Blu-Ray, TV shows on <a href="https://www.tomsguide.com/reviews/netflix">Netflix</a>, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-5">Movies</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Vd96znQ6EEjdaQVZ2ey3un" name="JBL Bar 1300XMK2 8.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Vd96znQ6EEjdaQVZ2ey3un.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As always, I watched “Top Gun: Maverick”. The opening scene was bassy and powerful; the male vocals in “Danger Zone” led the soundscape, with the delicate metallic clinking and whooshing ropes present in the high-end. </p><p>Every flying scene was particularly outstanding. The first scene, with Maverick flying in hypersonic, rumbled through the subwoofer with immense energy and depth. Each siren blare and alarm was clear over the pounding bass. Equally, the Bar 1300XMK2 balanced high-frequency diegetic sounds like rustling papers and ticking clocks with similarly-diegetic dialogue perfectly.  </p><p>Dolby Atmos performance was also phenomenal. In the first dogfighting scene, the planes raced overhead as if I were truly outside. The soundtrack was bombastic without overpowering the screeching jet engine sounds, and the radio dialogue was surprisingly clear. I wanted a <em>touch</em> more width in the rears, even when I increased the surrounds’ volume to max — but this is me being pedantic. The overall performance was astounding. </p><p>Next, I watched “The Wild Robot”, which has exemplary Dolby Atmos performance. During the swimming scenes, I could hear the main character goose splashing in the water as if the water was all around me. </p><p>After, I watched “No Time To Die”. The opening scene with the wind whistling over the snowy lake was fantastic; the crunching snow underfoot was delicately balanced over the tense, string-heavy OST. The shattering lake sounded piercing and, of course, the famous car chase scene sounded powerful and dramatic. I actually physically jumped when the grenade exploded. The subwoofer was putting in overtime; every bullet, engine rev, and collision made the entire room shake. </p><h2 id="tv-5">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="baHUhNnozLDkMBHuWeBp4n" name="JBL Bar 1300XMK2 9.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/baHUhNnozLDkMBHuWeBp4n.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the JBL Bar 1300XMK2, I watched several TV shows on Netflix, NOWTV (a U.K. streaming platform), and Disney+. NOWTV has full Dolby Atmos, so really brought the best out of the Bar 1300XMK2. </p><p>When I watched “Heated Rivalry”, the skating sound effects were clear over the synth-heavy electronic soundtrack. Softer diegetic sounds like clothes ruffling and clinking of cutlery in restaurant scenes were audible and separated from dialogue and soundtrack. This is one of the best surround-sound soundbars I’ve ever tested. dialogue felt like it was coming from around me, and scene texture effects were balanced and natural. No notes — this was a perfect experience.</p><p>I also watched “Euphoria”. The dialogue, soundtrack, and bullet sound effects were all clear. I never needed to rewind for missed dialogue even in loud scenes, nightclub scenes, or action scenes. </p><h2 id="music-5">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="MwGYP8UBsDoXuTEYiv83km" name="JBL Bar 1300XMK2 3.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/MwGYP8UBsDoXuTEYiv83km.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This is kind of cheating, because it’s the theme song of “No Time To Die” (one of the movies I used to test the 1300XMK2), but it sounded utterly marvelous through this soundbar. Billie Eilish’s voice was melodic, textured, and I could hear her every breath, every layer of her vocal cords. The bass and percussion in the second verse were balanced and emotive. </p><p>On Qobuz, I played “Hexagons” by Muse, a track built over arpeggiated guitar notes and rolling synths. The vocals were clear and emotive, just like the previous track, and the percussion was clear. The bass wasn’t as well-rounded as you’d expect from some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best speakers</a>, but, of course, this is a soundbar.</p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-verdict"><span>JBL Bar 1300XMK2 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XRGwSE7nSZ3NGj97mMmYtn" name="JBL Bar 1300XMK2 4.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/XRGwSE7nSZ3NGj97mMmYtn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Overall, I have nothing bad to say about the JBL Bar 1300XMK2. Sure, the 4K pass-through is limited to 60Hz, but if you’re not a gamer, this might not be an issue. For me, and most other movie lovers, you’ll be oh so pleased with the Bar 13000XMK2. </p><p>I loved the previous version, and one of its two flaws has been fixed: audio doesn’t stutter after long pauses. While the second “flaw”, its lack of 4K pass-through at 120Hz, remains, I’m not overly concerned with this for movie lovers. Gamers might need to check out something like the <a href="https://www.amazon.com/Samsung-Soundbar-HW-Q990H-Subwoofer-Q-Symphony/dp/B0GL1Q8P1P">Samsung Q990H</a> ($1,1997).</p><p>If you value movie-theater-accurate sound, a wide soundstage, immersive Dolby Atmos, and great smart features (AirPlay, calibration), then the JBL Bar 1300XMK2 is the one for you. This is one of the easiest ways to get a full home theater experience at home.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/jbl-bar-1300xmk2-review</link>
                                                                            <description>
                            <![CDATA[ The JBL Bar 1300XMK2 is one of the best ways to get a full cinematic audio experience at home, with a powerful subwoofer, immersive Atmos, and clear dialogue. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">JVDWv89cjhLWnxkVMxdT2N</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/Eh4CBhBUUki6FxW9Pqg5Bn-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 10 Jul 2026 12:11:59 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/Eh4CBhBUUki6FxW9Pqg5Bn-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the jbl bar 1300xmk2 in black photographed against the blue tom&#039;s guide background]]></media:description>                                                            <media:text><![CDATA[the jbl bar 1300xmk2 in black photographed against the blue tom&#039;s guide background]]></media:text>
                                <media:title type="plain"><![CDATA[the jbl bar 1300xmk2 in black photographed against the blue tom&#039;s guide background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/Eh4CBhBUUki6FxW9Pqg5Bn-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The JBL Bar 1300XMK2 has a lot to live up to. Its predecessor completely blew me away with its movie-theater-accurate sound and unbeatable ease of use. So is the MK2 enough to be one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a>? </p><p>If you couldn’t tell from the 4.5-star rating, yes. Yes it is. This is the easiest (and, arguably, the most affordable) way to achieve cinematic Dolby Atmos at home. Dialogue is crystal-clear, even in bombastic action scenes, and high-frequency detail sounds aren’t swallowed by bass. The sound is just unbeatable at this price. </p><p>At $1,699, this isn’t a cheap product by any means. But if you know anything about home theater, you’ll know that this <em>actually</em> isn’t too expensive for an 11.1.4 channel configuration — with 25 individual drivers. Want to find out more? Keep reading this JBL Bar 1300XMK2 review. </p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-specs"><span>JBL Bar 1300XMK2 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow">$1,699</a> / <a href="https://www.amazon.co.uk/JBL-11-1-4-Channel-Bluetooth-Detachable-Subwoofer/dp/B0FH2RPQVB" target="_blank" rel="nofollow">£1,299</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI, Bluetooth, Wi-Fi, USB, Optical</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>Soundbar: 13 pounds / Subwoofer: 26 pounds / Rears: 2.8 pounds each</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>Soundbar: 40.6" x 2.3" x 5.4" / Subwoofer: 12.4” x 10.9” x 10.8” / Rears: 8" x 2.3" x 5.4"</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>11.1.4</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-price-availability"><span>JBL Bar 1300XMK2 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ksgHdzPCWF3QxG7kfhymqn" name="JBL Bar 1300XMK2 6.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/ksgHdzPCWF3QxG7kfhymqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The JBL Bar 13000XMK2 is <a href="https://www.amazon.com/JBL-1300XMK2-11-1-4-soundbar-Detachable-Surround/dp/B0FN1JLNCN" target="_blank" rel="nofollow">$1,699 from Amazon U.S.</a> In the U.K., you’re looking at an MSRP of £1,299, but it’s currently “Coming Soon” on JBL’s site. Strangely, though, you can grab it from <a href="https://www.amazon.co.uk/JBL-11-1-4-Channel-Bluetooth-Detachable-Subwoofer/dp/B0FH2RPQVB" target="_blank" rel="nofollow">Amazon U.K. for around £1,160</a>.</p><p>Although this four-digit MSRP <em>looks</em> expensive, I promise you, it’s worth every penny and is actually cheaper than similarly spec’d soundbars. The <em>power</em> in this speaker is almost unparalleled. Of all the soundbars I’ve tested, only the $900 <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> has a better performance-per-penny ratio. </p><p>I’ve also tested the <a href="https://www.tomsguide.com/audio/soundbars/kef-xio-review">KEF XIO</a> and the <a href="https://www.tomsguide.com/audio/soundbars/yamaha-true-x-surround-90a-review">Yamaha True X Surround 90A </a>($3,499), both incredible soundbars in their own right, but both will set you back considerably more than the JBL — without a sub and rears, too. </p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-design"><span>JBL Bar 1300XMK2 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uQDNtcvzR6tMJZuy2Zyvqn" name="JBL Bar 1300XMK2 2.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/uQDNtcvzR6tMJZuy2Zyvqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In a word, the JBL Bar 1300XMK2 is powerful. This soundbar was rattling my floorboards, vibrating my skeleton, shaking my walls. With a whopping 2470W of power, this is a killer sound system. Thankfully, the soundbar also knows where to <em>put </em>that power. </p><p>There are a whopping 23 drivers in this 11.1.4 channel setup. The subwoofer itself has two 8-inch drivers, on top of the 19 drivers in the soundbar itself. In real terms, this means the 1300XMK2 is a behemoth, with sound worthy of a movie theater.</p><p>In terms of looks, the 1300XMK2 is your average soundbar. It’s not particularly gorgeous or particularly revolting. The bar itself is 40 inches long, so I’d recommend using it only if you have a 65” TV and up, otherwise it might look strange. </p><p>The subwoofer is <em>huge</em> and a frankly massive 26 pounds, so, again, only get this if you have space. </p><h2 id="controls-8">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="u9eBdprQrXoC4mK2dCGLZn" name="JBL_.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/u9eBdprQrXoC4mK2dCGLZn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>You can control the JBL Bar 1300XMK2 via either the remote or your phone. The remote is long and lightweight, with pleasantly tactile buttons. It’s much bigger than Bose’s soundbar remote, but feels more plasticky than Samsung’s. </p><p>Unlike the KEF XIO, you can control the soundbar settings while watching TV. You can adjust subwoofer volume and surround volume directly from your phone. I found this responded instantly with zero stuttering. </p><p>The on-bar buttons are minimal: just volume up and down. I don’t miss the buttons on the bar — I only really ever use my phone and remote anyway. </p><h2 id="features-5">Features</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TcKVby8SNBkZ5mjCbzZpqn" name="JBL Bar 1300XMK2 1.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/TcKVby8SNBkZ5mjCbzZpqn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ll discuss the app features in detail just below, but the Bar 1300XMK2 has a huge range of features. There’s automatic room calibration, broadcast mode, EQ adjustment, night mode, PureVoice mode, and smart mode. </p><p>One feature is missing, though: 4K / 120Hz pass-through for gaming. The Bar 1300XMK2's pass-through HDMI port is limited to 60Hz, so if you wanted to run your console through the soundbar (if you’re short on HDMI ports), unfortunately, you won’t be able to play at a 120fps frame rate in 4K. Not great news for FPS or other fast-paced games.</p><p>In fairness, this feature isn't ubiquitous with soundbars, and it'll only be an issue if you have no HDMI 2.1 port free on your TV. But this was a complaint we had about the previous model, so it's disappointing to see no improvement here on the new one.</p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-app"><span>JBL Bar 1300XMK2 review: App</span></h2><p>I’ll go through all the main features one by one: room calibration is exactly as it sounds. The soundbar sends out sonic pulses to the rears, which helps position the sound waves for 360° audio. This was the first thing I did when I unboxed the soundbar and it made my movies sound super immersive. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yVPWZ85RHnqGWcYgbNntGR" name="jbl bar 1300 x screenshots" alt="the JBL Bar 1300XMK2 app connectivity showing features and room calibration of the soundbar and subwoofer and surrounds" src="https://cdn.mos.cms.futurecdn.net/yVPWZ85RHnqGWcYgbNntGR.png" mos="" align="middle" fullscreen="" width="1600" height="900" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / JBL)</span></figcaption></figure><p>As you can see, the JBL One app is well-designed and visually appealing, and I experienced no connectivity issues. </p><p>Next: broadcast mode. This enables you to keep the surround speakers connected to the soundbar even when you’re in different rooms. So, in theory, you could listen to a movie playing in the living room while you’re in the bathtub. Not sure why you’d want to, but you can. </p><p>You can also connect your phone to the surrounds and use them as individual speakers, and they have Auracast. In theory, you can link them up to JBL speakers like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">Xtreme 5</a>.</p><p>Of course, night mode and EQ adjustments are exactly what you might think — night mode reduces bass for fewer subwoofer-related sleep interruptions, and EQ allows you to completely customize the soundbar’s output. </p><p>Smart mode is interesting — it’s effectively a “standard” “Music” and “Movie” mode you can toggle to come on automatically or manually. Smart mode adjusts the sound mode based on the input. For example, if it detects a DVD player, it will activate movie mode, and if you play via a <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">music streaming service</a>, it will activate music. </p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-sound-quality"><span>JBL Bar 1300XMK2 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="86pjqE4NiTp36zWkQ5hfYn" name="JBL Bar 1300XMK2 5.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/86pjqE4NiTp36zWkQ5hfYn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I test every soundbar with “Top Gun: Maverick” on 4K Blu-Ray, TV shows on <a href="https://www.tomsguide.com/reviews/netflix">Netflix</a>, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-5">Movies</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Vd96znQ6EEjdaQVZ2ey3un" name="JBL Bar 1300XMK2 8.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Vd96znQ6EEjdaQVZ2ey3un.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As always, I watched “Top Gun: Maverick”. The opening scene was bassy and powerful; the male vocals in “Danger Zone” led the soundscape, with the delicate metallic clinking and whooshing ropes present in the high-end. </p><p>Every flying scene was particularly outstanding. The first scene, with Maverick flying in hypersonic, rumbled through the subwoofer with immense energy and depth. Each siren blare and alarm was clear over the pounding bass. Equally, the Bar 1300XMK2 balanced high-frequency diegetic sounds like rustling papers and ticking clocks with similarly-diegetic dialogue perfectly.  </p><p>Dolby Atmos performance was also phenomenal. In the first dogfighting scene, the planes raced overhead as if I were truly outside. The soundtrack was bombastic without overpowering the screeching jet engine sounds, and the radio dialogue was surprisingly clear. I wanted a <em>touch</em> more width in the rears, even when I increased the surrounds’ volume to max — but this is me being pedantic. The overall performance was astounding. </p><p>Next, I watched “The Wild Robot”, which has exemplary Dolby Atmos performance. During the swimming scenes, I could hear the main character goose splashing in the water as if the water was all around me. </p><p>After, I watched “No Time To Die”. The opening scene with the wind whistling over the snowy lake was fantastic; the crunching snow underfoot was delicately balanced over the tense, string-heavy OST. The shattering lake sounded piercing and, of course, the famous car chase scene sounded powerful and dramatic. I actually physically jumped when the grenade exploded. The subwoofer was putting in overtime; every bullet, engine rev, and collision made the entire room shake. </p><h2 id="tv-5">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="baHUhNnozLDkMBHuWeBp4n" name="JBL Bar 1300XMK2 9.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/baHUhNnozLDkMBHuWeBp4n.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the JBL Bar 1300XMK2, I watched several TV shows on Netflix, NOWTV (a U.K. streaming platform), and Disney+. NOWTV has full Dolby Atmos, so really brought the best out of the Bar 1300XMK2. </p><p>When I watched “Heated Rivalry”, the skating sound effects were clear over the synth-heavy electronic soundtrack. Softer diegetic sounds like clothes ruffling and clinking of cutlery in restaurant scenes were audible and separated from dialogue and soundtrack. This is one of the best surround-sound soundbars I’ve ever tested. dialogue felt like it was coming from around me, and scene texture effects were balanced and natural. No notes — this was a perfect experience.</p><p>I also watched “Euphoria”. The dialogue, soundtrack, and bullet sound effects were all clear. I never needed to rewind for missed dialogue even in loud scenes, nightclub scenes, or action scenes. </p><h2 id="music-5">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="MwGYP8UBsDoXuTEYiv83km" name="JBL Bar 1300XMK2 3.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/MwGYP8UBsDoXuTEYiv83km.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This is kind of cheating, because it’s the theme song of “No Time To Die” (one of the movies I used to test the 1300XMK2), but it sounded utterly marvelous through this soundbar. Billie Eilish’s voice was melodic, textured, and I could hear her every breath, every layer of her vocal cords. The bass and percussion in the second verse were balanced and emotive. </p><p>On Qobuz, I played “Hexagons” by Muse, a track built over arpeggiated guitar notes and rolling synths. The vocals were clear and emotive, just like the previous track, and the percussion was clear. The bass wasn’t as well-rounded as you’d expect from some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best speakers</a>, but, of course, this is a soundbar.</p><h2 class="article-body__section" id="section-jbl-bar-1300xmk2-review-verdict"><span>JBL Bar 1300XMK2 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XRGwSE7nSZ3NGj97mMmYtn" name="JBL Bar 1300XMK2 4.JPG" alt="the jbl bar 1300xmk2 in black photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/XRGwSE7nSZ3NGj97mMmYtn.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Overall, I have nothing bad to say about the JBL Bar 1300XMK2. Sure, the 4K pass-through is limited to 60Hz, but if you’re not a gamer, this might not be an issue. For me, and most other movie lovers, you’ll be oh so pleased with the Bar 13000XMK2. </p><p>I loved the previous version, and one of its two flaws has been fixed: audio doesn’t stutter after long pauses. While the second “flaw”, its lack of 4K pass-through at 120Hz, remains, I’m not overly concerned with this for movie lovers. Gamers might need to check out something like the <a href="https://www.amazon.com/Samsung-Soundbar-HW-Q990H-Subwoofer-Q-Symphony/dp/B0GL1Q8P1P">Samsung Q990H</a> ($1,1997).</p><p>If you value movie-theater-accurate sound, a wide soundstage, immersive Dolby Atmos, and great smart features (AirPlay, calibration), then the JBL Bar 1300XMK2 is the one for you. This is one of the easiest ways to get a full home theater experience at home.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The Marshall Acton IV won me over with its charming retro design and incredible sound — and it’s earned a permanent spot on my coffee table ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Marshall’s done it once again. The Marshall Acton IV is the latest addition to the fourth iteration of Homeline, the brand’s lineup of home speakers. Now featuring RCA ports for connecting turntables as well as Auracast for pairing it with other Homeline speakers, the Acton IV is one of the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a> around. Energetic but controlled bass combined with refined mids and clean treble results in a truly enjoyable listening experience.</p><p>The Acton IV sports Marshall’s retro and classy aesthetics and is a sight to behold. Amp-like controls, a user-friendly companion app, and room-filling sound make it a winner. But there’s still no voice assistant support and no Wi-Fi streaming — both of which are offered by the likes of Sonos and Denon.</p><p>Should you still buy it? Read my full Marshall Acton IV review to find out.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-specs"><span>Marshall Acton IV review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.marshall.com/gb/en/product/acton-iv" target="_blank" rel="nofollow">$299 / £259</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, 3.5mm wired, RCA</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes, unlimited with Heddon hub</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>2x 25W Class D amplifiers (tweeters), 1x 60W Class D amplifier (woofer), 1x 4-inch woofer, 2x 0.75-inch tweeters</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>37Hz-38KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>10m</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>Mains-powered</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>10 x 5.9 x 5.6 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>5.84lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Cream</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-marshall-acton-iv-review-price-availability"><span>Marshall Acton IV review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XaAGrLM5abfVS7uZLmFktZ" name="Marshall_Acton_IV_.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/XaAGrLM5abfVS7uZLmFktZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Launched in July 2026, the Marshall Acton IV succeeds the four-year-old Acton III. It joins the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Marshall Stanmore IV</a> ($399) to form the fourth iteration of the British brand’s Homeline series — its lineup of mains-powered home speakers. The Acton IV has a retail price of <a href="https://www.marshall.com/gb/en/product/acton-iv" target="_blank" rel="nofollow">$299 / £259</a>, the same as the Acton III at launch.</p><p>The Acton IV’s sub-$300 price puts it firmly in the mid-range category — and it’s a great Sonos alternative. It’s a lot cheaper than both the <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos Era 300</a> ($449) and the <a href="https://www.tomsguide.com/audio/smart-speakers/denon-home-350-review">Denon Home 350</a> ($749) but you also sacrifice voice assistant and Wi-Fi support by saving on the Acton IV. The Acton IV is a little pricier compared to the<a href="https://www.tomsguide.com/audio/bluetooth-speakers/audio-pro-addon-c5-mkii-review"> Audio Pro Addon C5 MKII</a> ($219) and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/klipsch-the-one-plus-review">Klipsch The One Plus</a> ($274) but in my opinion, Marshall’s offering is worth the extra dough for its looks alone.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-design"><span>Marshall Acton IV review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="48izeDu2V7iY3ZD6pAzdSa" name="Marshall_Acton_IV_ 6.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/48izeDu2V7iY3ZD6pAzdSa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It’s hard to argue against the Marshall Acton IV’s styling. Similar to other Marshall speakers, the Acton IV sports a retro, stylish design that’s sure to draw eyeballs to it — in the best way possible. The speaker is available in Black or Cream primary colors, each with brass and gray accents.The gray mesh grille that covers the front, along with the brass Marshall logo, adds a vintage feel to the Acton IV.</p><p>Similar to many of Marshall’s other speakers, the Acton IV doesn’t sport any garish RGB lighting so it looks classy. The only lighting you get is the red LED indicators located on the top plate, which indicate the different active settings.</p><p>The Acton IV is designed to stay put in one place. It’s a mains-powered home speaker, after all. It measures roughly 10 x 5.9 x 5.6 inches and weighs 5.84lbs, so it isn’t exactly portable. It isn’t <em>too </em>heavy, though, as I’ve carried it to and from the office in my backpack. Compared to the Sonos Era 300 which sports a more rounded shape, the Acton IV is a rectangle, and it’s nearly the same size as the Audio Pro Addon C5 MKII, but smaller than the Klipsch The One Plus. It didn’t take up much space on my bookshelf or my coffee table.</p><h2 id="weatherproofing-2">Weatherproofing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YV4f3Da9gkTDKmTAJqpwfZ" name="Marshall_Acton_IV_ 2.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/YV4f3Da9gkTDKmTAJqpwfZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Acton IV is a home speaker designed for indoors, and as such, it doesn’t have an official IP waterproof rating. Also, because it needs to be plugged into mains, I wouldn’t recommend using it in the shower or even in your garden when it’s raining. If you want a weatherproof speaker, you may want to consider one of the <a href="https://www.tomsguide.com/best-picks/best-waterproof-speakers">best waterproof Bluetooth speakers</a>, like the IP68-rated <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a> ($399) or the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> ($39) if you’re on a tight budget. If you want a rugged speaker in Marshall’s style, the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-middleton-ii-review">Marshall Middleton II</a> ($329) is IP67-rated while the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review">Marshall Kilburn III </a>($379) is IP54-rated.</p><h2 id="controls-9">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aAn4Bj8mNMxqzgzxh7F3Qa" name="Marshall_Acton_IV_ 4.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/aAn4Bj8mNMxqzgzxh7F3Qa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Keeping in line with Marshall’s design philosophy, the Acton IV features amp-like physical controls located on the top plate. They’re all fairly straightforward to understand. You get a Source button to swap between AUX, RCA and Bluetooth inputs; three dials to adjust the volume, bass and treble; and a joystick to go to the next or previous song as well as play/pause playback.</p><p>I appreciate having onboard bass and treble controls, both of which can be adjusted on a scale of one to 10. These negate the need for the companion app (more on that shortly), as you don’t need to fire it up every time you want to adjust the equalizer. All the buttons and knobs give great tactile feedback when pressed or turned.</p><p>There’s a customizable M button, too, which is also found on the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Marshall Bromley 750</a> party speaker ($1,299) and the <a href="https://www.tomsguide.com/audio/headphones/marshall-milton-anc-review">Marshall Milton ANC</a> headphones ($229). Via the app, you can customize it to change the EQ or activate Spotify Tap to instantly start playing music without toggling it on your phone.</p><h2 id="connectivity-5">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:960px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BuQYjznEoFCo9yWEHboUWa" name="Marshall_Acton_IV_ 3.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/v2/t:435,l:142,cw:960,ch:540,q:80/BuQYjznEoFCo9yWEHboUWa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ll start this off by saying that no, the Acton IV does not feature Wi-Fi connectivity or voice assistant support, like the Sonos Era 300 and the Denon Home 350 do. This isn’t rare in this price range, as the Klipsch The One Plus doesn’t feature Wi-Fi streaming either. You may want to consider either the Denon or the Sonos if you want superior wireless streaming with lossless hi-res playback.</p><p>Now that that’s out of the way, let’s talk about what the Acton IV <em>does </em>have. The Acton IV utilizes Bluetooth 5.3 to connect to your smartphone, laptop or other Bluetooth-enabled device. There’s support for SBC, AAC, LC3 and LDAC codecs, which gives it a leg-up over the Klipsch The One Plus and the Audio Pro Addon C5 MKII, both of which feature SBC and AAC support only. Compared to AAC and SBC, LC3 offers better efficiency and stability, while LDAC operates at a higher maximum bitrate, thereby minimizing detail loss. In lieu of Wi-Fi streaming, you can use LDAC for hi-res streaming instead.</p><p>Connecting via Bluetooth is extremely easy as, when you power on the Acton IV for the first time, it immediately puts itself into pairing mode. For connecting new devices, all you need to do is hold down the Source button.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FdQMWGCjKQULU6MxdHWuRa" name="Marshall_Acton_IV_ 7.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/FdQMWGCjKQULU6MxdHWuRa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Wired connectivity is in spades, too. Located next to the controls is a 3.5mm AUX port, and on the Acton IV’s underside, you’ll find RCA ports (which you won’t on the Acton III) for plugging in the <a href="https://www.tomsguide.com/best-picks/best-record-players">best turntables</a>, CD players, or even a TV. Thanks to the Source button, you can keep all your devices connected and swap between inputs as and when needed.</p><p>The Acton IV is Auracast-enabled, like its predecessor. But a new feature is that you can now connect the Acton IV to other Homeline speakers, like the Stanmore IV. I tested this as I had both speakers at hand, and it worked like a charm. I placed the Acton IV in my living room and the Stanmore IV in my bedroom, and I was able to play the same tunes through both. Setting this up took just under five minutes, too.</p><p>If you want to connect old Homeline speakers with it, like the Acton III and the Stanmore III, you’ll need to purchase the <a href="https://www.marshall.com/gb/en/product/heddon">Marshall Heddon</a> physical hub ($299) which uses Wi-Fi to transmit to other Marshall speakers via Auracast. If you buy two or more Homeline speakers together, Marshall happily throws in the Heddon for free.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-sound-quality"><span>Marshall Acton IV review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Ko9LQnFb3AdB8UUG8ruiNG" name="Marshall_Acton_IV-2" alt="A black Marshall Acton IV on a bookshelf" src="https://cdn.mos.cms.futurecdn.net/Ko9LQnFb3AdB8UUG8ruiNG.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Though it be but little, it is fierce… or whatever it was that William Shakespeare said. The Marshall Acton IV is fitted with two 25W Class D amplifiers which power its dual 0.75-inch tweeters, as well as one 60W Class D amplifier to power its 4-inch woofer. The speaker covers a frequency response range of 37Hz to 38KHz. Marshall says that it has improved the tweeters and waveguides to create “a better, wider soundstage” — and that it does.</p><p>To test the Acton IV, I listened to my usual array of testing songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res audio. I kicked things off with Lana Del Rey’s ‘<a href="https://open.qobuz.com/track/203578478" target="_blank" rel="nofollow">Taco Truck x VB</a>’ with the bass and treble set to five each. The song’s first half landed with a warm, almost analog character that suits the Acton IV’s tuning. Lana’s vocals sat front and centre without sounding too clinical, and every breath felt textured and layered. When the song pivoted into the second section, the beat dropped with rounded, confident low-end impact, and the kick drum had plenty of <em>oomph</em> to it. The track’s dynamism was replicated beautifully and it sounded highly immersive.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9uT5bdXUWaLvxbWVXro2Xa" name="Marshall_Acton_IV_ 5.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/9uT5bdXUWaLvxbWVXro2Xa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the balance between treble and bass, I listened to ‘<a href="https://open.qobuz.com/track/291554006" target="_blank" rel="nofollow">Elephant</a>’ by 070 Shake. The treble remained smooth throughout, as the shimmering synth and ambient effects lit up the soundstage without becoming fatiguing. The bass sounded powerful but controlled, and even though the track doesn’t feature overwhelming sub-bass, the Acton IV gave each note satisfying weight. The song felt alive, and having listened to the entirety of Shake’s album ‘<a href="https://open.qobuz.com/album/pev0y4s7ocsfa" target="_blank" rel="nofollow">Petrichor</a>’ on it, it felt like the speaker was built for her sound.</p><p>The Acton IV looks and feels like a rock and roll speaker, so I had to give ‘<a href="https://open.qobuz.com/track/102006913" target="_blank" rel="nofollow">Fear of a Blank Planet</a>’ by Porcupine Tree a couple of spins. The opening electric guitars entered with real substance, with the distorted riffs carrying a distinctive crunch. As the drums kicked in, the Acton IV made the dense recording with layered guitars, intricate drumming, and understated vocals sound extremely energetic. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BuQYjznEoFCo9yWEHboUWa" name="Marshall_Acton_IV_ 3.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/BuQYjznEoFCo9yWEHboUWa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Marshall has also made tweaks to what it calls Dynamic Loudness. It says that subtle adjustments have been made to the high and low frequencies and by doing so, music sounds free of distortion and remains consistent at all volume levels. Having tested the speaker by listening at the lowest and highest volume, I can confirm that this is almost true.</p><p>The aforementioned songs sounded just as detailed even at low volumes. I found volume between two and four ideal for listening to <a href="https://open.qobuz.com/track/252730752" target="_blank" rel="nofollow">lo-fi tracks</a> when concentrating on writing this review. But at the highest volume, the tracks sounded a little distorted and muddy, and not particularly enjoyable. Also, bear in mind that the Acton IV can get <em>very </em>loud. When I was listening to it on the highest volume, I was certain my neighbors were going to knock on my door and tell me to turn it down!</p><p><em>Listen along to my review with the Marshall Acton IV testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/66553804?zone=GB-en"></iframe><h2 class="article-body__section" id="section-marshall-acton-iv-review-app"><span>Marshall Acton IV review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="2vPEDXHy2XcsrqMXx7yx9B" name="Marshall_Acton_IV-app" alt="Marshall app" src="https://cdn.mos.cms.futurecdn.net/2vPEDXHy2XcsrqMXx7yx9B.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/2vPEDXHy2XcsrqMXx7yx9B.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Marshall / Tom's Guide)</span></figcaption></figure><p>The Marshall Acton IV is accompanied by the <a href="https://play.google.com/store/apps/details?id=com.marshallgroup.marshall&hl=en_GB" target="_blank" rel="nofollow">Marshall</a> app on iOS and Android. The app sports a clean interface with plenty of signposting, and it gives you plenty of control over the Acton IV’s settings. You can customize the five band equalizer or choose one of five EQ presets, assign a function to the ‘M’ button, adjust the brightness of the LED indicators, and update the Acton IV’s firmware.</p><p>In addition to all this, you also get access to Placement Compensation where you can tailor the sound based on whether the speaker is close to an edge or flush against a wall or corner. The speaker then corrects for any nearby reflective surfaces that may affect the sound. It’s worth spending a few minutes fiddling with Placement Compensation to get the best sound depending on the speaker’s placement.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-verdict"><span>Marshall Acton IV review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="78qLi4onJjLDUSzHzuUxXa" name="Marshall_Acton_IV_ 1.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/78qLi4onJjLDUSzHzuUxXa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Acton IV is designed for music lovers needing a good-looking home speaker that can get very loud. Sporting a beautiful design, the Acton IV is sure to add a retro charm to your living space. But that’s not even its biggest selling point — it’s the room-filling detailed sound. The bass is powerful but controlled, while the treble is vibrant without ever becoming fatiguing.</p><p>In addition to Bluetooth connectivity, you get RCA input too for plugging in your record player. While there’s no Wi-Fi streaming, which both Denon and Sonos offer, the Acton IV is Auracast-enabled and can be connected with other Homeline speakers to create a multi-speaker setup. There’s no voice assistant, though, which may also have you considering alternatives.</p><p>But if you want a speaker that sounds stupendous and you don’t care about smart features, then the Acton IV should be your port of call. For its looks alone, it has earned a permanent spot on my coffee table.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/marshall-acton-iv-review</link>
                                                                            <description>
                            <![CDATA[ The Marshall Acton IV is a charming, retro-styled home speaker that boasts detailed sound. It can be paired with other Homeline IV speakers too. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">iSDX7wqV9QtvWLE4rjKtXT</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/XaAGrLM5abfVS7uZLmFktZ-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 07 Jul 2026 12:00:00 +0000</pubDate>                                                                                                                                <updated>Fri, 14 Aug 2026 10:23:54 +0000</updated>
                                                                                                                                            <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/XaAGrLM5abfVS7uZLmFktZ-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Marshall Acton IV mains-powered home speaker]]></media:description>                                                            <media:text><![CDATA[A black Marshall Acton IV mains-powered home speaker]]></media:text>
                                <media:title type="plain"><![CDATA[A black Marshall Acton IV mains-powered home speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/XaAGrLM5abfVS7uZLmFktZ-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Marshall’s done it once again. The Marshall Acton IV is the latest addition to the fourth iteration of Homeline, the brand’s lineup of home speakers. Now featuring RCA ports for connecting turntables as well as Auracast for pairing it with other Homeline speakers, the Acton IV is one of the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a> around. Energetic but controlled bass combined with refined mids and clean treble results in a truly enjoyable listening experience.</p><p>The Acton IV sports Marshall’s retro and classy aesthetics and is a sight to behold. Amp-like controls, a user-friendly companion app, and room-filling sound make it a winner. But there’s still no voice assistant support and no Wi-Fi streaming — both of which are offered by the likes of Sonos and Denon.</p><p>Should you still buy it? Read my full Marshall Acton IV review to find out.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-specs"><span>Marshall Acton IV review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.marshall.com/gb/en/product/acton-iv" target="_blank" rel="nofollow">$299 / £259</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, 3.5mm wired, RCA</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes, unlimited with Heddon hub</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>2x 25W Class D amplifiers (tweeters), 1x 60W Class D amplifier (woofer), 1x 4-inch woofer, 2x 0.75-inch tweeters</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>37Hz-38KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>10m</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>Mains-powered</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>10 x 5.9 x 5.6 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>5.84lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Cream</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-marshall-acton-iv-review-price-availability"><span>Marshall Acton IV review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XaAGrLM5abfVS7uZLmFktZ" name="Marshall_Acton_IV_.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/XaAGrLM5abfVS7uZLmFktZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Launched in July 2026, the Marshall Acton IV succeeds the four-year-old Acton III. It joins the <a href="https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review">Marshall Stanmore IV</a> ($399) to form the fourth iteration of the British brand’s Homeline series — its lineup of mains-powered home speakers. The Acton IV has a retail price of <a href="https://www.marshall.com/gb/en/product/acton-iv" target="_blank" rel="nofollow">$299 / £259</a>, the same as the Acton III at launch.</p><p>The Acton IV’s sub-$300 price puts it firmly in the mid-range category — and it’s a great Sonos alternative. It’s a lot cheaper than both the <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos Era 300</a> ($449) and the <a href="https://www.tomsguide.com/audio/smart-speakers/denon-home-350-review">Denon Home 350</a> ($749) but you also sacrifice voice assistant and Wi-Fi support by saving on the Acton IV. The Acton IV is a little pricier compared to the<a href="https://www.tomsguide.com/audio/bluetooth-speakers/audio-pro-addon-c5-mkii-review"> Audio Pro Addon C5 MKII</a> ($219) and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/klipsch-the-one-plus-review">Klipsch The One Plus</a> ($274) but in my opinion, Marshall’s offering is worth the extra dough for its looks alone.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-design"><span>Marshall Acton IV review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="48izeDu2V7iY3ZD6pAzdSa" name="Marshall_Acton_IV_ 6.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/48izeDu2V7iY3ZD6pAzdSa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It’s hard to argue against the Marshall Acton IV’s styling. Similar to other Marshall speakers, the Acton IV sports a retro, stylish design that’s sure to draw eyeballs to it — in the best way possible. The speaker is available in Black or Cream primary colors, each with brass and gray accents.The gray mesh grille that covers the front, along with the brass Marshall logo, adds a vintage feel to the Acton IV.</p><p>Similar to many of Marshall’s other speakers, the Acton IV doesn’t sport any garish RGB lighting so it looks classy. The only lighting you get is the red LED indicators located on the top plate, which indicate the different active settings.</p><p>The Acton IV is designed to stay put in one place. It’s a mains-powered home speaker, after all. It measures roughly 10 x 5.9 x 5.6 inches and weighs 5.84lbs, so it isn’t exactly portable. It isn’t <em>too </em>heavy, though, as I’ve carried it to and from the office in my backpack. Compared to the Sonos Era 300 which sports a more rounded shape, the Acton IV is a rectangle, and it’s nearly the same size as the Audio Pro Addon C5 MKII, but smaller than the Klipsch The One Plus. It didn’t take up much space on my bookshelf or my coffee table.</p><h2 id="weatherproofing-2">Weatherproofing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YV4f3Da9gkTDKmTAJqpwfZ" name="Marshall_Acton_IV_ 2.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/YV4f3Da9gkTDKmTAJqpwfZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Acton IV is a home speaker designed for indoors, and as such, it doesn’t have an official IP waterproof rating. Also, because it needs to be plugged into mains, I wouldn’t recommend using it in the shower or even in your garden when it’s raining. If you want a weatherproof speaker, you may want to consider one of the <a href="https://www.tomsguide.com/best-picks/best-waterproof-speakers">best waterproof Bluetooth speakers</a>, like the IP68-rated <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a> ($399) or the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> ($39) if you’re on a tight budget. If you want a rugged speaker in Marshall’s style, the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-middleton-ii-review">Marshall Middleton II</a> ($329) is IP67-rated while the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review">Marshall Kilburn III </a>($379) is IP54-rated.</p><h2 id="controls-9">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aAn4Bj8mNMxqzgzxh7F3Qa" name="Marshall_Acton_IV_ 4.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/aAn4Bj8mNMxqzgzxh7F3Qa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Keeping in line with Marshall’s design philosophy, the Acton IV features amp-like physical controls located on the top plate. They’re all fairly straightforward to understand. You get a Source button to swap between AUX, RCA and Bluetooth inputs; three dials to adjust the volume, bass and treble; and a joystick to go to the next or previous song as well as play/pause playback.</p><p>I appreciate having onboard bass and treble controls, both of which can be adjusted on a scale of one to 10. These negate the need for the companion app (more on that shortly), as you don’t need to fire it up every time you want to adjust the equalizer. All the buttons and knobs give great tactile feedback when pressed or turned.</p><p>There’s a customizable M button, too, which is also found on the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Marshall Bromley 750</a> party speaker ($1,299) and the <a href="https://www.tomsguide.com/audio/headphones/marshall-milton-anc-review">Marshall Milton ANC</a> headphones ($229). Via the app, you can customize it to change the EQ or activate Spotify Tap to instantly start playing music without toggling it on your phone.</p><h2 id="connectivity-5">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:960px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BuQYjznEoFCo9yWEHboUWa" name="Marshall_Acton_IV_ 3.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/v2/t:435,l:142,cw:960,ch:540,q:80/BuQYjznEoFCo9yWEHboUWa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ll start this off by saying that no, the Acton IV does not feature Wi-Fi connectivity or voice assistant support, like the Sonos Era 300 and the Denon Home 350 do. This isn’t rare in this price range, as the Klipsch The One Plus doesn’t feature Wi-Fi streaming either. You may want to consider either the Denon or the Sonos if you want superior wireless streaming with lossless hi-res playback.</p><p>Now that that’s out of the way, let’s talk about what the Acton IV <em>does </em>have. The Acton IV utilizes Bluetooth 5.3 to connect to your smartphone, laptop or other Bluetooth-enabled device. There’s support for SBC, AAC, LC3 and LDAC codecs, which gives it a leg-up over the Klipsch The One Plus and the Audio Pro Addon C5 MKII, both of which feature SBC and AAC support only. Compared to AAC and SBC, LC3 offers better efficiency and stability, while LDAC operates at a higher maximum bitrate, thereby minimizing detail loss. In lieu of Wi-Fi streaming, you can use LDAC for hi-res streaming instead.</p><p>Connecting via Bluetooth is extremely easy as, when you power on the Acton IV for the first time, it immediately puts itself into pairing mode. For connecting new devices, all you need to do is hold down the Source button.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FdQMWGCjKQULU6MxdHWuRa" name="Marshall_Acton_IV_ 7.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/FdQMWGCjKQULU6MxdHWuRa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Wired connectivity is in spades, too. Located next to the controls is a 3.5mm AUX port, and on the Acton IV’s underside, you’ll find RCA ports (which you won’t on the Acton III) for plugging in the <a href="https://www.tomsguide.com/best-picks/best-record-players">best turntables</a>, CD players, or even a TV. Thanks to the Source button, you can keep all your devices connected and swap between inputs as and when needed.</p><p>The Acton IV is Auracast-enabled, like its predecessor. But a new feature is that you can now connect the Acton IV to other Homeline speakers, like the Stanmore IV. I tested this as I had both speakers at hand, and it worked like a charm. I placed the Acton IV in my living room and the Stanmore IV in my bedroom, and I was able to play the same tunes through both. Setting this up took just under five minutes, too.</p><p>If you want to connect old Homeline speakers with it, like the Acton III and the Stanmore III, you’ll need to purchase the <a href="https://www.marshall.com/gb/en/product/heddon">Marshall Heddon</a> physical hub ($299) which uses Wi-Fi to transmit to other Marshall speakers via Auracast. If you buy two or more Homeline speakers together, Marshall happily throws in the Heddon for free.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-sound-quality"><span>Marshall Acton IV review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Ko9LQnFb3AdB8UUG8ruiNG" name="Marshall_Acton_IV-2" alt="A black Marshall Acton IV on a bookshelf" src="https://cdn.mos.cms.futurecdn.net/Ko9LQnFb3AdB8UUG8ruiNG.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Though it be but little, it is fierce… or whatever it was that William Shakespeare said. The Marshall Acton IV is fitted with two 25W Class D amplifiers which power its dual 0.75-inch tweeters, as well as one 60W Class D amplifier to power its 4-inch woofer. The speaker covers a frequency response range of 37Hz to 38KHz. Marshall says that it has improved the tweeters and waveguides to create “a better, wider soundstage” — and that it does.</p><p>To test the Acton IV, I listened to my usual array of testing songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res audio. I kicked things off with Lana Del Rey’s ‘<a href="https://open.qobuz.com/track/203578478" target="_blank" rel="nofollow">Taco Truck x VB</a>’ with the bass and treble set to five each. The song’s first half landed with a warm, almost analog character that suits the Acton IV’s tuning. Lana’s vocals sat front and centre without sounding too clinical, and every breath felt textured and layered. When the song pivoted into the second section, the beat dropped with rounded, confident low-end impact, and the kick drum had plenty of <em>oomph</em> to it. The track’s dynamism was replicated beautifully and it sounded highly immersive.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9uT5bdXUWaLvxbWVXro2Xa" name="Marshall_Acton_IV_ 5.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/9uT5bdXUWaLvxbWVXro2Xa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the balance between treble and bass, I listened to ‘<a href="https://open.qobuz.com/track/291554006" target="_blank" rel="nofollow">Elephant</a>’ by 070 Shake. The treble remained smooth throughout, as the shimmering synth and ambient effects lit up the soundstage without becoming fatiguing. The bass sounded powerful but controlled, and even though the track doesn’t feature overwhelming sub-bass, the Acton IV gave each note satisfying weight. The song felt alive, and having listened to the entirety of Shake’s album ‘<a href="https://open.qobuz.com/album/pev0y4s7ocsfa" target="_blank" rel="nofollow">Petrichor</a>’ on it, it felt like the speaker was built for her sound.</p><p>The Acton IV looks and feels like a rock and roll speaker, so I had to give ‘<a href="https://open.qobuz.com/track/102006913" target="_blank" rel="nofollow">Fear of a Blank Planet</a>’ by Porcupine Tree a couple of spins. The opening electric guitars entered with real substance, with the distorted riffs carrying a distinctive crunch. As the drums kicked in, the Acton IV made the dense recording with layered guitars, intricate drumming, and understated vocals sound extremely energetic. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BuQYjznEoFCo9yWEHboUWa" name="Marshall_Acton_IV_ 3.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/BuQYjznEoFCo9yWEHboUWa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Marshall has also made tweaks to what it calls Dynamic Loudness. It says that subtle adjustments have been made to the high and low frequencies and by doing so, music sounds free of distortion and remains consistent at all volume levels. Having tested the speaker by listening at the lowest and highest volume, I can confirm that this is almost true.</p><p>The aforementioned songs sounded just as detailed even at low volumes. I found volume between two and four ideal for listening to <a href="https://open.qobuz.com/track/252730752" target="_blank" rel="nofollow">lo-fi tracks</a> when concentrating on writing this review. But at the highest volume, the tracks sounded a little distorted and muddy, and not particularly enjoyable. Also, bear in mind that the Acton IV can get <em>very </em>loud. When I was listening to it on the highest volume, I was certain my neighbors were going to knock on my door and tell me to turn it down!</p><p><em>Listen along to my review with the Marshall Acton IV testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/66553804?zone=GB-en"></iframe><h2 class="article-body__section" id="section-marshall-acton-iv-review-app"><span>Marshall Acton IV review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="2vPEDXHy2XcsrqMXx7yx9B" name="Marshall_Acton_IV-app" alt="Marshall app" src="https://cdn.mos.cms.futurecdn.net/2vPEDXHy2XcsrqMXx7yx9B.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/2vPEDXHy2XcsrqMXx7yx9B.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Marshall / Tom's Guide)</span></figcaption></figure><p>The Marshall Acton IV is accompanied by the <a href="https://play.google.com/store/apps/details?id=com.marshallgroup.marshall&hl=en_GB" target="_blank" rel="nofollow">Marshall</a> app on iOS and Android. The app sports a clean interface with plenty of signposting, and it gives you plenty of control over the Acton IV’s settings. You can customize the five band equalizer or choose one of five EQ presets, assign a function to the ‘M’ button, adjust the brightness of the LED indicators, and update the Acton IV’s firmware.</p><p>In addition to all this, you also get access to Placement Compensation where you can tailor the sound based on whether the speaker is close to an edge or flush against a wall or corner. The speaker then corrects for any nearby reflective surfaces that may affect the sound. It’s worth spending a few minutes fiddling with Placement Compensation to get the best sound depending on the speaker’s placement.</p><h2 class="article-body__section" id="section-marshall-acton-iv-review-verdict"><span>Marshall Acton IV review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="78qLi4onJjLDUSzHzuUxXa" name="Marshall_Acton_IV_ 1.JPG" alt="A black Marshall Acton IV mains-powered home speaker" src="https://cdn.mos.cms.futurecdn.net/78qLi4onJjLDUSzHzuUxXa.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Marshall Acton IV is designed for music lovers needing a good-looking home speaker that can get very loud. Sporting a beautiful design, the Acton IV is sure to add a retro charm to your living space. But that’s not even its biggest selling point — it’s the room-filling detailed sound. The bass is powerful but controlled, while the treble is vibrant without ever becoming fatiguing.</p><p>In addition to Bluetooth connectivity, you get RCA input too for plugging in your record player. While there’s no Wi-Fi streaming, which both Denon and Sonos offer, the Acton IV is Auracast-enabled and can be connected with other Homeline speakers to create a multi-speaker setup. There’s no voice assistant, though, which may also have you considering alternatives.</p><p>But if you want a speaker that sounds stupendous and you don’t care about smart features, then the Acton IV should be your port of call. For its looks alone, it has earned a permanent spot on my coffee table.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ ‘Easy on the eyes and ridiculously stunning sound’ — I tested the Marshall Stanmore IV home speaker and it’s coming for Sonos’ crown ]]></title>
                                                                                                <dc:content><![CDATA[ <p>If, like me, you love listening to music when you’re chilling, cooking or doing basically anything else indoors, you need one of the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a> — and few are better than the Marshall Stanmore IV. The fourth iteration of Stanmore improves on its predecessor by introducing enhanced bass response, plus a wide soundstage to deliver an immersive listening experience.</p><p>The Stanmore IV is well-designed and extremely well-built, and it features Bluetooth, RCA and AUX inputs for most, if not all, devices you own. The bass is powerful while the treble is smooth and defined, and the speaker can get very loud without distortion. While it’s missing Wi-Fi streaming and voice assistant support — something its closest rival, Sonos, offers — this is <em>the </em>home speaker to get if you want full-bodied sound above all else.</p><p>For the complete breakdown, read my full Marshall Stanmore IV review.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-specs"><span>Marshall Stanmore IV review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.marshall.com/gb/en/product/stanmore-iv" target="_blank" rel="nofollow">$399 / £349</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, 3.5mm wired, RCA</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes, unlimited with Heddon hub</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>2x 25W Class D amplifiers (tweeters), 1x 60W Class D amplifier (woofer), 1x 5-inch woofer, 2x 0.75-inch tweeters</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>36Hz-38KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>10m</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>Mains-powered</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>13.78 x 7.99 x 7.28 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>8.81lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Cream</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-price-availability"><span>Marshall Stanmore IV review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="ZUwziSGGrdiYfhEmvUKcyC" name="Marshall_Stanmore_IV-4" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/ZUwziSGGrdiYfhEmvUKcyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Marshall Stanmore IV was announced on 7 July 2026 along with the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-acton-iv-review">Marshall Acton IV</a>, both of which form the brand’s lineup of home speakers, known as Homeline IV. It’s the fourth iteration of and the successor to the Marshall Stanmore III which came out four years ago. The Stanmore IV has a retail price of <a href="https://www.marshall.com/gb/en/product/stanmore-iv" target="_blank" rel="nofollow">$399 / £349</a> — so it’s $20 pricier than the Stanmore III’s launch price.</p><p>The Stanmore IV is also pricier than the Acton IV ($299), but that’s the extra you pay for a bigger speaker and no distortion when playing music at the highest volume. Price- and features-wise, the Stanmore IV competes with the <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos Era 300</a> ($449) and the <a href="https://www.tomsguide.com/audio/i-test-audio-gear-for-a-living-and-this-wireless-speaker-just-showed-me-a-better-way-to-fill-my-house-with-music">Klipsch The Three Plus</a> ($399). The Era 300 could be worth the extra spend if you want Wi-Fi streaming and voice assistant support, though, neither of which the Stanmore IV can manage.</p><p>Denon’s Home lineup is another rival, but with the <a href="https://www.tomsguide.com/audio/smart-speakers/denon-home-350-review">Denon Home 350</a> ($749) and the Denon Home 600 ($799) costing nearly double, they may be out of your budget. The Stanmore IV is, then, an excellent speaker for those who don’t want to spend a fortune on full-bodied sound.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-design"><span>Marshall Stanmore IV review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="uZBwedpn8WDvTyxxFeZE2D" name="Marshall_Stanmore_IV-5" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/uZBwedpn8WDvTyxxFeZE2D.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Marshall Stanmore IV is a chonky boi, and as a mains-powered speaker, it’s meant to be kept plugged in and in one spot. Measuring 13.78 x 7.99 x 7.28 inches and weighing a whopping 8.81lbs, I highly doubt you’ll be lugging it around unless you’re moving houses. The Stanmore IV is a little bigger than the Klipsch The Three Plus and significantly bigger than the rounded Sonos Era 300. It fits nicely on my bookshelf and window sill.</p><p>There's no doubt that Marshall knows how to make a good-looking speaker. We’ve seen it with the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Bromley 450</a> ($799), the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review">Kilburn III</a> ($379), and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-middleton-ii-review">Middleton II</a> ($329), to name a few. The Stanmore IV is no different. It shares similar design philosophies to its stablemate, the Acton IV. It’s available in black or cream, with brass and gray accents to deliver one of the most stunning speakers I’ve had the pleasure of testing.</p><p>Adding to the speaker’s minimalist charm is its lack of any garish RGB lighting, and any form of illumination is limited to the red LED settings indicators on the top plate. Build quality is top-notch so you’re getting bang for your buck here. From the faux-leather vinyl finish to the quality of the knobs and buttons, Marshall has spared no expense to deliver what feels like a long-lasting speaker.</p><h2 id="weatherproofing-3">Weatherproofing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="4Q9nW4PJMr3fcdmDw4v92D" name="Marshall_Stanmore_IV-6" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/4Q9nW4PJMr3fcdmDw4v92D.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Just like the Acton IV, the Stanmore IV is a mains-powered home speaker designed for indoor use. It doesn’t have an official IP waterproof rating so I wouldn’t recommend using it in the bathroom or outside in the rain. You may want to consider one of the <a href="https://www.tomsguide.com/best-picks/best-waterproof-speakers">best waterproof Bluetooth speakers </a>for outdoor use, such as the IP67-rated Marshall Middleton II, the IP68-rated <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a> ($399), or the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> ($39) if you’re on a really tight budget.</p><h2 id="controls-10">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="gD69mKBPCpMW5n4tTd57yC" name="Marshall_Stanmore_IV-10" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/gD69mKBPCpMW5n4tTd57yC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Controls on the Stanmore IV are identical to the Acton IV’s. The dials and buttons are located on the top plate. These include the Source button to swap between Bluetooth, AUX and RCA inputs; three dials for adjusting the bass, treble and volume on a scale of one to 10 each; a joystick for changing tracks and pausing playback; and a customizable ‘M’ button that, by default, plays Spotify, thereby negating the need to launch the app via your phone.</p><p>All the controls are easy to understand and don’t take long to get a hang of. Having onboard bass and treble dials is especially handy as you don’t need to fire up the companion app every time to adjust these. The ‘M’ button can also be customized to swap between EQ presets. The controls also give great tactile feedback when pressed — and the amp-like power button makes me feel like a rockstar every time I turn on the Stanmore IV.</p><h2 id="connectivity-6">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="H8GuBkSvChMhBJ7yjbGshC" name="Marshall_Stanmore_IV-8" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/H8GuBkSvChMhBJ7yjbGshC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Similar to the Acton IV, the Stanmore IV doesn’t feature Wi-Fi streaming or voice assistant support. These are home speakers and while they look very smart, they aren’t <em>smart </em>speakers, so you may want to consider the Sonos Era 300 if you want superior, lossless hi-res playback. The Klipsch The Three Plus doesn’t feature Wi-Fi connectivity either, so you’re limited to Bluetooth playback on both speakers.</p><p>The Stanmore IV utilizes Bluetooth 5.3 with support for SBC, AAC, LC3 and LDAC codecs — the Klipsch The Three Plus supports only SBC and AAC. LC3 offers better stability and efficiency compared to SBC and AAC, and LDAC can be used for hi-res streaming in the absence of Wi-Fi… although Wi-Fi streaming is still more hi-res than Bluetooth LDAC because it doesn’t compress the music files. Again, you’ll need to spend the extra $50 on the Sonos Era 300 for that.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="A9ynqFbYgou9PudNXGBWzC" name="Marshall_Stanmore_IV-7" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/A9ynqFbYgou9PudNXGBWzC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Alongside wireless connectivity, you also get a 3.5mm AUX port located next to the controls, and RCA ports for connecting your <a href="https://www.tomsguide.com/best-picks/best-record-players">record player</a> on the speaker’s underside. You can keep all your devices plugged in and simply use the Source button to swap between them as and when needed. The ports are now located on the speaker’s underside, as opposed to on the back of its predecessor. This means that you can now place the Stanmore IV (and the Acton IV) flush against walls without compromising on space or sound quality.</p><p>The Stanmore IV is Auracast-enabled and can now be connected to its Homeline IV sibling — its predecessor could pair with another Stanmore III only, so this is a new feature. I was testing both the Acton IV and the Stanmore IV at the same time so I connected them together which took mere seconds, and it worked like a charm.</p><p>If you already own Homeline III speakers, you can grab the <a href="https://www.marshall.com/gb/en/product/heddon" target="_blank" rel="nofollow">Marshall Heddon hub for $299</a> to connect all your speakers together. The Heddon uses Wi-Fi to transmit to other Marshall speakers, making it worth the investment for multi-room setups. If you buy two or more Homeline speakers, Marshall will throw in the Heddon for free, too.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-sound-quality"><span>Marshall Stanmore IV review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Lkea6HfByzSmyHCyoBJgyC" name="Marshall_Stanmore_IV-2" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/Lkea6HfByzSmyHCyoBJgyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Marshall Stanmore IV is fitted with two 25W Class D amplifiers to power its dual 0.75-inch tweeters, and one 60W Class D amplifier for its 5-inch woofer, and it covers a frequency response range of 36Hz to 38KHz. I tested the speaker by listening to a few songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res audio — and I’ve been thoroughly impressed.</p><p>Though it’s a single-box speaker, the Stanmore IV boasts a very wide soundstage, and I immediately noticed this when listening to Tame Impala’s ‘<a href="https://open.qobuz.com/track/8524915" target="_blank" rel="nofollow">Apocalypse Dreams</a>.’ The swirling synths, shimmering guitars and reverb-soaked vocals extended beyond the cabinet, delivering an almost three-dimensional quality. The speaker’s instrument separation is commendable as it allowed every effect and melody to remain distinct but without losing cohesion.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="5p8xWyUbUNSFSL4BCdNm8C" name="Marshall_Stanmore_IV-11" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/5p8xWyUbUNSFSL4BCdNm8C.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>I then listened to ‘<a href="https://open.qobuz.com/track/161150244" target="_blank" rel="nofollow">Romeo and Juliet</a>’ by Dire Straits, and it was perhaps the best the song has ever sounded to me. The opening resonator guitar sounded beautifully textured with crisp, shimmering detail, and the treble never once turned harsh. The Stanmore IV’s warm, rich tuning complements the song perfectly, lending it an analog feel while preserving subtle nuances, such as Mark Knopfler’s breath work. As the arrangement swelled, the Stanmore IV handled the dynamics effortlessly, allowing the percussion instruments to occupy their own space without overpowering the acoustic guitar.</p><p>Bass on the Stanmore IV is deep and controlled, as was made apparent to me when listening to ‘<a href="https://open.qobuz.com/track/2329144" target="_blank" rel="nofollow">Intro</a>’ by The xx. The iconic bassline held real weight and filled the room without becoming bloated or overpowering. Each note plucked on the bass started and stopped cleanly which helped the song maintain its hypnotic rhythm. It made me feel like that scene of Jon Hamm dancing at the club in ‘Your Friends & Neighbors.’</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Dxf6Avd2mkhriYhCfXPftP" name="Marshall_Stanmore_IV" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/Dxf6Avd2mkhriYhCfXPftP.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Stanmore IV can get <em>very </em>loud too — louder than you’ll realistically need when listening to music indoors. But what I appreciate about the Stanmore IV’s loudness is that even at the highest volume, music doesn’t sound distorted or muddy — an issue I had with the Acton IV. Marshall’s fine-tuned Dynamic Loudness comes into play here, where the brand has made subtle adjustments to the high and low frequencies, thereby ensuring that music sounds free of distortion regardless of the volume.</p><p>If you want a pure rock ‘n’ roll sound that’s consistent across the volume spectrum, few speakers are better than the Stanmore IV.</p><p><em>Listen along to my review with the Marshall Stanmore IV testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/66713202?zone=GB-en"></iframe><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-app"><span>Marshall Stanmore IV review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="S2zsTeeuSQHEGwSGeTYWiW" name="Marshall_Stanmore_IV-App" alt="Marshall app screenshots" src="https://cdn.mos.cms.futurecdn.net/S2zsTeeuSQHEGwSGeTYWiW.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/S2zsTeeuSQHEGwSGeTYWiW.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Marshall / Tom's Guide)</span></figcaption></figure><p>To customize the Marshall Stanmore IV, you’ll need to download the <a href="https://play.google.com/store/apps/details?id=com.marshallgroup.marshall&hl=en_GB" target="_blank" rel="nofollow">Marshall</a> app on either iOS or Android. Sporting a clean interface with plenty of signposting, the Marshall app is easy to navigate and enables you to adjust nearly every Marshall device’s settings. You can use it to customize the Stanmore IV’s five-band equalizer, assign a custom function to the ‘M’ button, adjust the brightness of the LED indicators, and of course, update the firmware.</p><p>Marshall has also added a Placement Compensation function to the app. You can select whether the speaker is close to an edge or corner, or flush against a wall, and the Stanmore IV will then correct for any nearby reflective surfaces that may affect the sound quality. It’s quick to do and changes are instantaneous, so it’s worth taking a couple of minutes and playing around with it to get the best possible sound.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-verdict"><span>Marshall Stanmore IV review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ihaxNtPXv45ssfqDnniGyC" name="Marshall_Stanmore_IV-3" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/ihaxNtPXv45ssfqDnniGyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Home speakers are perfect for, well, at-home listening, and the Marshall Stanmore IV delivers beautiful, immersive sound quality that doesn’t give you much to complain about. It’s an easy speaker to recommend if your priorities are premium design and superb sound quality over smart features, like voice assistant support. The wide, room-filling soundstage, powerful yet controlled bass, and vibrant treble make the Stanmore IV a joy to listen to.</p><p>Adding to the Stanmore IV’s charm are its amp-like tactile controls and drool-worthy design. Unlike its smaller sibling, the Acton IV, the Stanmore IV maintains distortion-free sound even at high volumes, making it a great choice for soundtracking dinner parties. Throw in versatile wired inputs and Auracast for connecting other Homeline IV speakers and you’ve got yourself a winner.</p><p>If you aren’t fussed about the lack of Wi-Fi streaming and voice assistant support, and can’t swing the extra dough on Sonos or Denon, the Stanmore IV is an exceptional speaker. Sometimes, sounding brilliant is all a home speaker really needs to do.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/marshall-stanmore-iv-review</link>
                                                                            <description>
                            <![CDATA[ The Marshall Stanmore IV is an exceptional home speaker, boasting powerful bass and refined treble regardless of the volume. It looks beautiful and retro, too. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">FwbTkPNNyUP7qUBGbxTTo7</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/ihaxNtPXv45ssfqDnniGyC-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 07 Jul 2026 12:00:00 +0000</pubDate>                                                                                                                                <updated>Fri, 14 Aug 2026 10:23:01 +0000</updated>
                                                                                                                                            <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/ihaxNtPXv45ssfqDnniGyC-1280-80.jpg">
                                                            <media:credit><![CDATA[Nikita Achanta / Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Marshall Stanmore IV home speaker]]></media:description>                                                            <media:text><![CDATA[A black Marshall Stanmore IV home speaker]]></media:text>
                                <media:title type="plain"><![CDATA[A black Marshall Stanmore IV home speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/ihaxNtPXv45ssfqDnniGyC-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>If, like me, you love listening to music when you’re chilling, cooking or doing basically anything else indoors, you need one of the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a> — and few are better than the Marshall Stanmore IV. The fourth iteration of Stanmore improves on its predecessor by introducing enhanced bass response, plus a wide soundstage to deliver an immersive listening experience.</p><p>The Stanmore IV is well-designed and extremely well-built, and it features Bluetooth, RCA and AUX inputs for most, if not all, devices you own. The bass is powerful while the treble is smooth and defined, and the speaker can get very loud without distortion. While it’s missing Wi-Fi streaming and voice assistant support — something its closest rival, Sonos, offers — this is <em>the </em>home speaker to get if you want full-bodied sound above all else.</p><p>For the complete breakdown, read my full Marshall Stanmore IV review.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-specs"><span>Marshall Stanmore IV review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.marshall.com/gb/en/product/stanmore-iv" target="_blank" rel="nofollow">$399 / £349</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3, 3.5mm wired, RCA</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes, unlimited with Heddon hub</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>2x 25W Class D amplifiers (tweeters), 1x 60W Class D amplifier (woofer), 1x 5-inch woofer, 2x 0.75-inch tweeters</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>36Hz-38KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>10m</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>Mains-powered</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>13.78 x 7.99 x 7.28 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>8.81lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Cream</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-price-availability"><span>Marshall Stanmore IV review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="ZUwziSGGrdiYfhEmvUKcyC" name="Marshall_Stanmore_IV-4" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/ZUwziSGGrdiYfhEmvUKcyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Marshall Stanmore IV was announced on 7 July 2026 along with the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-acton-iv-review">Marshall Acton IV</a>, both of which form the brand’s lineup of home speakers, known as Homeline IV. It’s the fourth iteration of and the successor to the Marshall Stanmore III which came out four years ago. The Stanmore IV has a retail price of <a href="https://www.marshall.com/gb/en/product/stanmore-iv" target="_blank" rel="nofollow">$399 / £349</a> — so it’s $20 pricier than the Stanmore III’s launch price.</p><p>The Stanmore IV is also pricier than the Acton IV ($299), but that’s the extra you pay for a bigger speaker and no distortion when playing music at the highest volume. Price- and features-wise, the Stanmore IV competes with the <a href="https://www.tomsguide.com/reviews/sonos-era-300">Sonos Era 300</a> ($449) and the <a href="https://www.tomsguide.com/audio/i-test-audio-gear-for-a-living-and-this-wireless-speaker-just-showed-me-a-better-way-to-fill-my-house-with-music">Klipsch The Three Plus</a> ($399). The Era 300 could be worth the extra spend if you want Wi-Fi streaming and voice assistant support, though, neither of which the Stanmore IV can manage.</p><p>Denon’s Home lineup is another rival, but with the <a href="https://www.tomsguide.com/audio/smart-speakers/denon-home-350-review">Denon Home 350</a> ($749) and the Denon Home 600 ($799) costing nearly double, they may be out of your budget. The Stanmore IV is, then, an excellent speaker for those who don’t want to spend a fortune on full-bodied sound.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-design"><span>Marshall Stanmore IV review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="uZBwedpn8WDvTyxxFeZE2D" name="Marshall_Stanmore_IV-5" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/uZBwedpn8WDvTyxxFeZE2D.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Marshall Stanmore IV is a chonky boi, and as a mains-powered speaker, it’s meant to be kept plugged in and in one spot. Measuring 13.78 x 7.99 x 7.28 inches and weighing a whopping 8.81lbs, I highly doubt you’ll be lugging it around unless you’re moving houses. The Stanmore IV is a little bigger than the Klipsch The Three Plus and significantly bigger than the rounded Sonos Era 300. It fits nicely on my bookshelf and window sill.</p><p>There's no doubt that Marshall knows how to make a good-looking speaker. We’ve seen it with the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Bromley 450</a> ($799), the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review">Kilburn III</a> ($379), and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-middleton-ii-review">Middleton II</a> ($329), to name a few. The Stanmore IV is no different. It shares similar design philosophies to its stablemate, the Acton IV. It’s available in black or cream, with brass and gray accents to deliver one of the most stunning speakers I’ve had the pleasure of testing.</p><p>Adding to the speaker’s minimalist charm is its lack of any garish RGB lighting, and any form of illumination is limited to the red LED settings indicators on the top plate. Build quality is top-notch so you’re getting bang for your buck here. From the faux-leather vinyl finish to the quality of the knobs and buttons, Marshall has spared no expense to deliver what feels like a long-lasting speaker.</p><h2 id="weatherproofing-3">Weatherproofing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="4Q9nW4PJMr3fcdmDw4v92D" name="Marshall_Stanmore_IV-6" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/4Q9nW4PJMr3fcdmDw4v92D.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Just like the Acton IV, the Stanmore IV is a mains-powered home speaker designed for indoor use. It doesn’t have an official IP waterproof rating so I wouldn’t recommend using it in the bathroom or outside in the rain. You may want to consider one of the <a href="https://www.tomsguide.com/best-picks/best-waterproof-speakers">best waterproof Bluetooth speakers </a>for outdoor use, such as the IP67-rated Marshall Middleton II, the IP68-rated <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-5-review">JBL Xtreme 5</a> ($399), or the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> ($39) if you’re on a really tight budget.</p><h2 id="controls-10">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="gD69mKBPCpMW5n4tTd57yC" name="Marshall_Stanmore_IV-10" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/gD69mKBPCpMW5n4tTd57yC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Controls on the Stanmore IV are identical to the Acton IV’s. The dials and buttons are located on the top plate. These include the Source button to swap between Bluetooth, AUX and RCA inputs; three dials for adjusting the bass, treble and volume on a scale of one to 10 each; a joystick for changing tracks and pausing playback; and a customizable ‘M’ button that, by default, plays Spotify, thereby negating the need to launch the app via your phone.</p><p>All the controls are easy to understand and don’t take long to get a hang of. Having onboard bass and treble dials is especially handy as you don’t need to fire up the companion app every time to adjust these. The ‘M’ button can also be customized to swap between EQ presets. The controls also give great tactile feedback when pressed — and the amp-like power button makes me feel like a rockstar every time I turn on the Stanmore IV.</p><h2 id="connectivity-6">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="H8GuBkSvChMhBJ7yjbGshC" name="Marshall_Stanmore_IV-8" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/H8GuBkSvChMhBJ7yjbGshC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Similar to the Acton IV, the Stanmore IV doesn’t feature Wi-Fi streaming or voice assistant support. These are home speakers and while they look very smart, they aren’t <em>smart </em>speakers, so you may want to consider the Sonos Era 300 if you want superior, lossless hi-res playback. The Klipsch The Three Plus doesn’t feature Wi-Fi connectivity either, so you’re limited to Bluetooth playback on both speakers.</p><p>The Stanmore IV utilizes Bluetooth 5.3 with support for SBC, AAC, LC3 and LDAC codecs — the Klipsch The Three Plus supports only SBC and AAC. LC3 offers better stability and efficiency compared to SBC and AAC, and LDAC can be used for hi-res streaming in the absence of Wi-Fi… although Wi-Fi streaming is still more hi-res than Bluetooth LDAC because it doesn’t compress the music files. Again, you’ll need to spend the extra $50 on the Sonos Era 300 for that.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="A9ynqFbYgou9PudNXGBWzC" name="Marshall_Stanmore_IV-7" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/A9ynqFbYgou9PudNXGBWzC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Alongside wireless connectivity, you also get a 3.5mm AUX port located next to the controls, and RCA ports for connecting your <a href="https://www.tomsguide.com/best-picks/best-record-players">record player</a> on the speaker’s underside. You can keep all your devices plugged in and simply use the Source button to swap between them as and when needed. The ports are now located on the speaker’s underside, as opposed to on the back of its predecessor. This means that you can now place the Stanmore IV (and the Acton IV) flush against walls without compromising on space or sound quality.</p><p>The Stanmore IV is Auracast-enabled and can now be connected to its Homeline IV sibling — its predecessor could pair with another Stanmore III only, so this is a new feature. I was testing both the Acton IV and the Stanmore IV at the same time so I connected them together which took mere seconds, and it worked like a charm.</p><p>If you already own Homeline III speakers, you can grab the <a href="https://www.marshall.com/gb/en/product/heddon" target="_blank" rel="nofollow">Marshall Heddon hub for $299</a> to connect all your speakers together. The Heddon uses Wi-Fi to transmit to other Marshall speakers, making it worth the investment for multi-room setups. If you buy two or more Homeline speakers, Marshall will throw in the Heddon for free, too.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-sound-quality"><span>Marshall Stanmore IV review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Lkea6HfByzSmyHCyoBJgyC" name="Marshall_Stanmore_IV-2" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/Lkea6HfByzSmyHCyoBJgyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Marshall Stanmore IV is fitted with two 25W Class D amplifiers to power its dual 0.75-inch tweeters, and one 60W Class D amplifier for its 5-inch woofer, and it covers a frequency response range of 36Hz to 38KHz. I tested the speaker by listening to a few songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res audio — and I’ve been thoroughly impressed.</p><p>Though it’s a single-box speaker, the Stanmore IV boasts a very wide soundstage, and I immediately noticed this when listening to Tame Impala’s ‘<a href="https://open.qobuz.com/track/8524915" target="_blank" rel="nofollow">Apocalypse Dreams</a>.’ The swirling synths, shimmering guitars and reverb-soaked vocals extended beyond the cabinet, delivering an almost three-dimensional quality. The speaker’s instrument separation is commendable as it allowed every effect and melody to remain distinct but without losing cohesion.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="5p8xWyUbUNSFSL4BCdNm8C" name="Marshall_Stanmore_IV-11" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/5p8xWyUbUNSFSL4BCdNm8C.jpg" mos="" align="middle" fullscreen="" width="2048" height="1151" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>I then listened to ‘<a href="https://open.qobuz.com/track/161150244" target="_blank" rel="nofollow">Romeo and Juliet</a>’ by Dire Straits, and it was perhaps the best the song has ever sounded to me. The opening resonator guitar sounded beautifully textured with crisp, shimmering detail, and the treble never once turned harsh. The Stanmore IV’s warm, rich tuning complements the song perfectly, lending it an analog feel while preserving subtle nuances, such as Mark Knopfler’s breath work. As the arrangement swelled, the Stanmore IV handled the dynamics effortlessly, allowing the percussion instruments to occupy their own space without overpowering the acoustic guitar.</p><p>Bass on the Stanmore IV is deep and controlled, as was made apparent to me when listening to ‘<a href="https://open.qobuz.com/track/2329144" target="_blank" rel="nofollow">Intro</a>’ by The xx. The iconic bassline held real weight and filled the room without becoming bloated or overpowering. Each note plucked on the bass started and stopped cleanly which helped the song maintain its hypnotic rhythm. It made me feel like that scene of Jon Hamm dancing at the club in ‘Your Friends & Neighbors.’</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Dxf6Avd2mkhriYhCfXPftP" name="Marshall_Stanmore_IV" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/Dxf6Avd2mkhriYhCfXPftP.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Stanmore IV can get <em>very </em>loud too — louder than you’ll realistically need when listening to music indoors. But what I appreciate about the Stanmore IV’s loudness is that even at the highest volume, music doesn’t sound distorted or muddy — an issue I had with the Acton IV. Marshall’s fine-tuned Dynamic Loudness comes into play here, where the brand has made subtle adjustments to the high and low frequencies, thereby ensuring that music sounds free of distortion regardless of the volume.</p><p>If you want a pure rock ‘n’ roll sound that’s consistent across the volume spectrum, few speakers are better than the Stanmore IV.</p><p><em>Listen along to my review with the Marshall Stanmore IV testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/66713202?zone=GB-en"></iframe><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-app"><span>Marshall Stanmore IV review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="S2zsTeeuSQHEGwSGeTYWiW" name="Marshall_Stanmore_IV-App" alt="Marshall app screenshots" src="https://cdn.mos.cms.futurecdn.net/S2zsTeeuSQHEGwSGeTYWiW.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/S2zsTeeuSQHEGwSGeTYWiW.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Marshall / Tom's Guide)</span></figcaption></figure><p>To customize the Marshall Stanmore IV, you’ll need to download the <a href="https://play.google.com/store/apps/details?id=com.marshallgroup.marshall&hl=en_GB" target="_blank" rel="nofollow">Marshall</a> app on either iOS or Android. Sporting a clean interface with plenty of signposting, the Marshall app is easy to navigate and enables you to adjust nearly every Marshall device’s settings. You can use it to customize the Stanmore IV’s five-band equalizer, assign a custom function to the ‘M’ button, adjust the brightness of the LED indicators, and of course, update the firmware.</p><p>Marshall has also added a Placement Compensation function to the app. You can select whether the speaker is close to an edge or corner, or flush against a wall, and the Stanmore IV will then correct for any nearby reflective surfaces that may affect the sound quality. It’s quick to do and changes are instantaneous, so it’s worth taking a couple of minutes and playing around with it to get the best possible sound.</p><h2 class="article-body__section" id="section-marshall-stanmore-iv-review-verdict"><span>Marshall Stanmore IV review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ihaxNtPXv45ssfqDnniGyC" name="Marshall_Stanmore_IV-3" alt="A black Marshall Stanmore IV home speaker" src="https://cdn.mos.cms.futurecdn.net/ihaxNtPXv45ssfqDnniGyC.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>Home speakers are perfect for, well, at-home listening, and the Marshall Stanmore IV delivers beautiful, immersive sound quality that doesn’t give you much to complain about. It’s an easy speaker to recommend if your priorities are premium design and superb sound quality over smart features, like voice assistant support. The wide, room-filling soundstage, powerful yet controlled bass, and vibrant treble make the Stanmore IV a joy to listen to.</p><p>Adding to the Stanmore IV’s charm are its amp-like tactile controls and drool-worthy design. Unlike its smaller sibling, the Acton IV, the Stanmore IV maintains distortion-free sound even at high volumes, making it a great choice for soundtracking dinner parties. Throw in versatile wired inputs and Auracast for connecting other Homeline IV speakers and you’ve got yourself a winner.</p><p>If you aren’t fussed about the lack of Wi-Fi streaming and voice assistant support, and can’t swing the extra dough on Sonos or Denon, the Stanmore IV is an exceptional speaker. Sometimes, sounding brilliant is all a home speaker really needs to do.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tested the minimalist Cambridge Audio L/R S — and they’re some of the most premium-feeling midrange speakers on the market ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Cambridge Audio L/R S are some of the <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html">best speakers</a> I’ve tested. At just 7 inches tall and 9 inches deep, these are relatively compact — but boy, do they sound larger than their measurements might suggest. </p><p>And just look at them. Aren’t they gorgeous? Not only do they look a treat, with the copper-colored tweeter and the smooth, almost tactile exterior, but they feel incredibly well-made too. Although quite pricey at <a href="https://www.amazon.com/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GWRKHVCS" target="_blank" rel="nofollow">$549</a>/<a href="https://www.amazon.co.uk/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GGJL9J9F" target="_blank" rel="nofollow">£399</a>, here you’re investing in sound, functionality, and appearance. </p><p>Sound is by far the most important thing when it comes to speakers, so you’ll be pleased to know that the L/R S sound divine. For me, they were missing a smidge of vivaciousness, but I still had a ball listening to them, and I’m sure 99.999% of you would, too. Want to find out more? Keep reading this Cambridge Audio L/R S review.</p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-specs"><span>Cambridge Audio L/R S review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GWRKHVCS" target="_blank" rel="nofollow">$549</a> / <a href="https://www.amazon.co.uk/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GGJL9J9F" target="_blank" rel="nofollow">£399</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth (aptX HD), RCA aux, USB-C, optical</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response rate</strong></p></td><td  ><p>60Hz - 20kHz</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer out</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>13.2 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>5.4 x 8.9 x 7.1 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>White, navy, black, wood</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-price-availability"><span>Cambridge Audio L/R S review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Voxa893BvXUogJ258QR5Jk" name="Cambridge_audio_ 9.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Voxa893BvXUogJ258QR5Jk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Cambridge L/R S are premium home speakers with a premium price. A pair will set you back <a href="https://www.amazon.com/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GWRKHVCS" target="_blank" rel="nofollow">$549 from Amazon U.S.</a> and <a href="https://www.amazon.co.uk/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GGJL9J9F" target="_blank" rel="nofollow">£399 from Amazon U.K</a>. Obviously, this is a much better deal in the U.K. — hi-fi just is inherently more expensive in the U.S., even without tariffs. </p><p>Cambridge Audio also makes the <a href="https://www.cambridgeaudio.com/gbr/en/products/speakers/lr/lr-m" target="_blank" rel="nofollow">L/R M</a> (£1,199), which work with Cambridge’s StreamMagic app and have built-in streaming capabilities. If you’ve got even more to spend, you could check out the L/R X (£1,799, releasing in July 2026), which are the L/R S on steroids. These are the aspirational older siblings of the L/R S, with dual woofers and proprietary tweeters. </p><p>If you want a similar performance for a little less, I’d recommend checking out the <a href="https://www.tomsguide.com/audio/speakers/edifier-s880db-mkii-review">Edifier S880DB MKII</a> ($449). Alternatively, you could look at the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review">Edifier R1280T</a> (my personal turntable speakers, $149), which don’t have Bluetooth but more than make up for it in affordability. Edifier does make a Bluetooth version — <a href="https://www.amazon.com/Edifier-R1280DB-Bluetooth-Bookshelf-Speakers/dp/B0CDPN5663" target="_blank" rel="nofollow">R1280DB</a> ($199), who’d have thought — for all those wireless lovers. </p><p>For those short on space, I’d go for the <a href="https://www.tomsguide.com/audio/speakers/i-just-tested-the-best-desktop-speakers-ive-heard-in-years-and-they-take-up-very-little-room">Kanto UKI</a> ($269) or the <a href="https://www.tomsguide.com/audio/speakers/edifier-m60-review">Edifier M60</a> ($199), both of which are compact speakers and sound great for their size. </p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-design"><span>Cambridge Audio L/R S review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZwzTGUwNSPy3QfZNahQSNk" name="Cambridge_audio_ 4.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/ZwzTGUwNSPy3QfZNahQSNk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Cambridge Audio L/R S are beautiful, beautiful speakers. They look uber modern, with no mesh grille covering and two visible drivers. The 21-mm aluminium tweeter is a gorgeous, rich copper color, and the 3-inch woofer meshes well with the speaker body. Whatever color you choose — white, black, navy, or wood — I bet it will look great in your home. </p><p>The speakers not only look great, but they feel well-made, too. The exterior is smooth, almost tactile, and the switches on the master speaker are high-quality. I have no issues with the design at all: everything is sleek and feels intentional.</p><h2 id="connectivity-7">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FCc99y3XV6JAFH3ivp3RNk" name="Cambridge_audio_ 5.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/FCc99y3XV6JAFH3ivp3RNk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As active speakers, you don’t need an amp or a phono if you’re using the speakers with a laptop or TV or phone. However, if you want to listen on your turntable, if it doesn’t have a phono pre-amp, you’ll need a separate one. </p><p>The L/R S have a huge array of connectivity types: Bluetooth aptX HD, RCA aux, optical, and USB-C playback. I mostly used USB-C and Bluetooth, both of which sounded excellent. </p><p>The speakers don’t come with a USB-C cable, an RCA cable, or an optical cable, so if you want to utilize these connectivity methods, you’ll have to supply them independently.</p><h2 id="controls-11">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2ryscYUmgtXYDaveafzDLk" name="Cambridge_audio_ 6.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/2ryscYUmgtXYDaveafzDLk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There are controls on the back of the master speaker and a high-quality remote. Rather than taking inspiration from Edifier’s slightly gimmicky circular touch remote, Cambridge Audio has (thankfully) gone back to basics with the L/R S’s remote. It’s a standard long remote with an equally standard layout. You’ve got play/pause, volume controls, EQ controls, and playback controls. </p><p>All very simple — but that’s what the L/R S are all about. They’re designed to let their quality speak for itself. </p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-audio-quality"><span>Cambridge Audio L/R S review: Audio quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Tx3Jbz6Fr2WPQseSg8wMUk" name="Cambridge_audio_ 8.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Tx3Jbz6Fr2WPQseSg8wMUk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the Cambridge Audio L/R S, I listened to a range of genres on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service for audiophiles</a>. I tested over both USB-C and Bluetooth. </p><p>I listened to “Getaway” by Fcukers, an electronic trip-hop-inspired track built over a cello riff, twinkling piano notes, and breathy female vocals. The tambourine shimmered gently at the back of the soundscape, allowing the string motifs to take centre stage without overpowering them. The bassline was controlled, tight, and balanced, and the space-gun sound effects tied everything together nicely. This song sounded great. </p><p>Next, I played “The Sickness in You and I” by Muse. This heavier rock track boasts crunchy nostalgic riffs and some fantastic stereo imaging. This song was an immersive marvel; when the percussion comes in on the left speaker with the guitars teeming across the soundscape, I wanted to replay it over and over. Despite being compact speakers, the stereo imaging of the L/R S is utterly excellent. If you listen to a lot of music mastered for stereo, these speakers will definitely get the best from your tunes.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JdsmzWhXar2sF6E8DFSfHk" name="Cambridge_audio_ 1.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/JdsmzWhXar2sF6E8DFSfHk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Last, I played “Never Get Enough” by Lianne La Havas, a track with acoustic verses and intense choruses. The speakers handled the change in bass well; the clarity of the acoustic guitar and La Havas’s voice was a standout in the verse, while the insistent bass came through well in the chorus.</p><p>Although most genres sounded excellent, I did wish for something <em>more</em>. The treble is clear, and there’s a surprising amount of bass for such small speakers. However, the mids aren’t as energetic as I personally like in speakers. Tracks sound balanced and clean, but they’re missing some vivaciousness and propulsiveness in the lower midrange. Don’t get me wrong — they still sound good, and you should still seriously consider them if you want immersive speakers with fantastic spatial imaging. But they didn’t pull at my heartstrings like other speakers have done in the past. </p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-verdict"><span>Cambridge Audio L/R S review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DEFDByxiwmmhto4HsojCXk" name="Cambridge_audio_ 7.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/DEFDByxiwmmhto4HsojCXk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Cambridge Audio L/R S are fantastic speakers. If you want a pair of home speakers that’ll make your music sound delectable, clear, and balanced, look no further. Even better if you value immersive spatial imaging above all else — these speakers will seriously impress you in that area. </p><p>The only thing holding the speakers back is a lack of <em>oomph </em>in the lower mids. For compact speakers, you can’t really expect perfection, though, and the L/R S still sound charming. If you need reliable, balanced speakers for your desk, turntable, TV, or anything in between, they’ll do you proud. </p><p>At $549/£399, they’re not exactly the cheapest speakers on the market, so I’d recommend only getting them if you’re serious about leveling up your home listening experience. But <em>what</em> a listening experience it would be. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/speakers/cambridge-audio-lr-s-review</link>
                                                                            <description>
                            <![CDATA[ The Cambridge Audio L/R S are high-quality bookshelf speakers with a huge range of connectivity types and clean sound. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">UWuV6NbgUn6RzHSyM2Dmwk</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/hcEZJ82cR7pqxLF4EDA9kk-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Wed, 01 Jul 2026 15:04:06 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/hcEZJ82cR7pqxLF4EDA9kk-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom&#039;s guide background]]></media:description>                                                            <media:text><![CDATA[the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom&#039;s guide background]]></media:text>
                                <media:title type="plain"><![CDATA[the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom&#039;s guide background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/hcEZJ82cR7pqxLF4EDA9kk-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Cambridge Audio L/R S are some of the <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html">best speakers</a> I’ve tested. At just 7 inches tall and 9 inches deep, these are relatively compact — but boy, do they sound larger than their measurements might suggest. </p><p>And just look at them. Aren’t they gorgeous? Not only do they look a treat, with the copper-colored tweeter and the smooth, almost tactile exterior, but they feel incredibly well-made too. Although quite pricey at <a href="https://www.amazon.com/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GWRKHVCS" target="_blank" rel="nofollow">$549</a>/<a href="https://www.amazon.co.uk/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GGJL9J9F" target="_blank" rel="nofollow">£399</a>, here you’re investing in sound, functionality, and appearance. </p><p>Sound is by far the most important thing when it comes to speakers, so you’ll be pleased to know that the L/R S sound divine. For me, they were missing a smidge of vivaciousness, but I still had a ball listening to them, and I’m sure 99.999% of you would, too. Want to find out more? Keep reading this Cambridge Audio L/R S review.</p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-specs"><span>Cambridge Audio L/R S review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GWRKHVCS" target="_blank" rel="nofollow">$549</a> / <a href="https://www.amazon.co.uk/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GGJL9J9F" target="_blank" rel="nofollow">£399</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth (aptX HD), RCA aux, USB-C, optical</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response rate</strong></p></td><td  ><p>60Hz - 20kHz</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer out</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>13.2 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>5.4 x 8.9 x 7.1 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>White, navy, black, wood</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-price-availability"><span>Cambridge Audio L/R S review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Voxa893BvXUogJ258QR5Jk" name="Cambridge_audio_ 9.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Voxa893BvXUogJ258QR5Jk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Cambridge L/R S are premium home speakers with a premium price. A pair will set you back <a href="https://www.amazon.com/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GWRKHVCS" target="_blank" rel="nofollow">$549 from Amazon U.S.</a> and <a href="https://www.amazon.co.uk/Cambridge-Audio-Built-Bluetooth-Positioning/dp/B0GGJL9J9F" target="_blank" rel="nofollow">£399 from Amazon U.K</a>. Obviously, this is a much better deal in the U.K. — hi-fi just is inherently more expensive in the U.S., even without tariffs. </p><p>Cambridge Audio also makes the <a href="https://www.cambridgeaudio.com/gbr/en/products/speakers/lr/lr-m" target="_blank" rel="nofollow">L/R M</a> (£1,199), which work with Cambridge’s StreamMagic app and have built-in streaming capabilities. If you’ve got even more to spend, you could check out the L/R X (£1,799, releasing in July 2026), which are the L/R S on steroids. These are the aspirational older siblings of the L/R S, with dual woofers and proprietary tweeters. </p><p>If you want a similar performance for a little less, I’d recommend checking out the <a href="https://www.tomsguide.com/audio/speakers/edifier-s880db-mkii-review">Edifier S880DB MKII</a> ($449). Alternatively, you could look at the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review">Edifier R1280T</a> (my personal turntable speakers, $149), which don’t have Bluetooth but more than make up for it in affordability. Edifier does make a Bluetooth version — <a href="https://www.amazon.com/Edifier-R1280DB-Bluetooth-Bookshelf-Speakers/dp/B0CDPN5663" target="_blank" rel="nofollow">R1280DB</a> ($199), who’d have thought — for all those wireless lovers. </p><p>For those short on space, I’d go for the <a href="https://www.tomsguide.com/audio/speakers/i-just-tested-the-best-desktop-speakers-ive-heard-in-years-and-they-take-up-very-little-room">Kanto UKI</a> ($269) or the <a href="https://www.tomsguide.com/audio/speakers/edifier-m60-review">Edifier M60</a> ($199), both of which are compact speakers and sound great for their size. </p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-design"><span>Cambridge Audio L/R S review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZwzTGUwNSPy3QfZNahQSNk" name="Cambridge_audio_ 4.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/ZwzTGUwNSPy3QfZNahQSNk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Cambridge Audio L/R S are beautiful, beautiful speakers. They look uber modern, with no mesh grille covering and two visible drivers. The 21-mm aluminium tweeter is a gorgeous, rich copper color, and the 3-inch woofer meshes well with the speaker body. Whatever color you choose — white, black, navy, or wood — I bet it will look great in your home. </p><p>The speakers not only look great, but they feel well-made, too. The exterior is smooth, almost tactile, and the switches on the master speaker are high-quality. I have no issues with the design at all: everything is sleek and feels intentional.</p><h2 id="connectivity-7">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FCc99y3XV6JAFH3ivp3RNk" name="Cambridge_audio_ 5.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/FCc99y3XV6JAFH3ivp3RNk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As active speakers, you don’t need an amp or a phono if you’re using the speakers with a laptop or TV or phone. However, if you want to listen on your turntable, if it doesn’t have a phono pre-amp, you’ll need a separate one. </p><p>The L/R S have a huge array of connectivity types: Bluetooth aptX HD, RCA aux, optical, and USB-C playback. I mostly used USB-C and Bluetooth, both of which sounded excellent. </p><p>The speakers don’t come with a USB-C cable, an RCA cable, or an optical cable, so if you want to utilize these connectivity methods, you’ll have to supply them independently.</p><h2 id="controls-11">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2ryscYUmgtXYDaveafzDLk" name="Cambridge_audio_ 6.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/2ryscYUmgtXYDaveafzDLk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There are controls on the back of the master speaker and a high-quality remote. Rather than taking inspiration from Edifier’s slightly gimmicky circular touch remote, Cambridge Audio has (thankfully) gone back to basics with the L/R S’s remote. It’s a standard long remote with an equally standard layout. You’ve got play/pause, volume controls, EQ controls, and playback controls. </p><p>All very simple — but that’s what the L/R S are all about. They’re designed to let their quality speak for itself. </p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-audio-quality"><span>Cambridge Audio L/R S review: Audio quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Tx3Jbz6Fr2WPQseSg8wMUk" name="Cambridge_audio_ 8.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/Tx3Jbz6Fr2WPQseSg8wMUk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test the Cambridge Audio L/R S, I listened to a range of genres on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, our <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service for audiophiles</a>. I tested over both USB-C and Bluetooth. </p><p>I listened to “Getaway” by Fcukers, an electronic trip-hop-inspired track built over a cello riff, twinkling piano notes, and breathy female vocals. The tambourine shimmered gently at the back of the soundscape, allowing the string motifs to take centre stage without overpowering them. The bassline was controlled, tight, and balanced, and the space-gun sound effects tied everything together nicely. This song sounded great. </p><p>Next, I played “The Sickness in You and I” by Muse. This heavier rock track boasts crunchy nostalgic riffs and some fantastic stereo imaging. This song was an immersive marvel; when the percussion comes in on the left speaker with the guitars teeming across the soundscape, I wanted to replay it over and over. Despite being compact speakers, the stereo imaging of the L/R S is utterly excellent. If you listen to a lot of music mastered for stereo, these speakers will definitely get the best from your tunes.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JdsmzWhXar2sF6E8DFSfHk" name="Cambridge_audio_ 1.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/JdsmzWhXar2sF6E8DFSfHk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Last, I played “Never Get Enough” by Lianne La Havas, a track with acoustic verses and intense choruses. The speakers handled the change in bass well; the clarity of the acoustic guitar and La Havas’s voice was a standout in the verse, while the insistent bass came through well in the chorus.</p><p>Although most genres sounded excellent, I did wish for something <em>more</em>. The treble is clear, and there’s a surprising amount of bass for such small speakers. However, the mids aren’t as energetic as I personally like in speakers. Tracks sound balanced and clean, but they’re missing some vivaciousness and propulsiveness in the lower midrange. Don’t get me wrong — they still sound good, and you should still seriously consider them if you want immersive speakers with fantastic spatial imaging. But they didn’t pull at my heartstrings like other speakers have done in the past. </p><h2 class="article-body__section" id="section-cambridge-audio-l-r-s-review-verdict"><span>Cambridge Audio L/R S review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DEFDByxiwmmhto4HsojCXk" name="Cambridge_audio_ 7.JPG" alt="the cambridge audio l/r s bookshelf speakers in white with a 21mm aluminum tweeter and a 3-inch woofer photographed against the blue tom's guide background" src="https://cdn.mos.cms.futurecdn.net/DEFDByxiwmmhto4HsojCXk.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Cambridge Audio L/R S are fantastic speakers. If you want a pair of home speakers that’ll make your music sound delectable, clear, and balanced, look no further. Even better if you value immersive spatial imaging above all else — these speakers will seriously impress you in that area. </p><p>The only thing holding the speakers back is a lack of <em>oomph </em>in the lower mids. For compact speakers, you can’t really expect perfection, though, and the L/R S still sound charming. If you need reliable, balanced speakers for your desk, turntable, TV, or anything in between, they’ll do you proud. </p><p>At $549/£399, they’re not exactly the cheapest speakers on the market, so I’d recommend only getting them if you’re serious about leveling up your home listening experience. But <em>what</em> a listening experience it would be. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Best Buy 4th of July sale: Score up to 33% off these 9 Bluetooth speakers from Sony, Klipsch, JBL, and more ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The deals just keep coming! Yes, Amazon's Prime Day sales may be over, but Best Buy is already running a huge 4th of July sale that's offering lots of great bargains. We covered the <a href="https://www.tomsguide.com/tvs/4th-of-july-tv-deals-save-up-to-usd1-700-on-lg-samsung-and-more-at-best-buy">Best Buy TV sale</a> earlier and now we're rounding up the best Bluetooth speakers, which you can get right now with some pretty deep discounts.</p><p>Bluetooth speakers come in all shapes and sizes and have a variety of price points to match. We've started our selection with some good value Soundcore and Marshall offerings that are ideal for taking to the beach or lounging by the pool. We've then turned our attention to some feature-packed offerings from the likes of JBL before rounding out with some seriously high-end speakers from the likes of Sony and Klipsch. </p><p>For more amazing deals, make sure to check out our guide to the best <a href="https://www.tomsguide.com/sales-events/best-4th-of-july-sales-2026-walmart-home-depot-more">4th of July sales</a>.</p>        <div class="featured_product_block featured_block_hero" data-id="712f8dac-c812-4827-ac4f-3e4e108c1cbc">            <a href="https://www.bestbuy.com/product/soundcore-by-anker-boom-2-ipx7-waterproof-floatable-80w-bluetooth-speaker-with-subwoofer-bassup-2-0-custom-rgb-light-effects-black/JJ858R2J8J" data-model-name="Soundcore by Anker Boom 2 Waterproof Floatable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/SFBcdMYYSfhFFS4dFidjba.jpg" alt="Soundcore by Anker Boom 2 Waterproof Floatable Bluetooth Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Soundcore</div>                                        <div class="featured__title">Soundcore by Anker Boom 2 Waterproof Floatable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="72e8b1f0-9065-4207-897c-b2786e91ab90">            <a href="https://www.bestbuy.com/product/marshall-emberton-iii-portable-bluetooth-speaker-with-rugged-waterproof-dust-proof-wireless-design-black-brass/CFCR4HZWQ3" data-model-name="Marshall Emberton III Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/pTDwusmBCM8F6nQVfBS4kj.jpg" alt="Marshall Emberton III Portable Bluetooth Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Marshall</div>                                        <div class="featured__title">Marshall Emberton III Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="da87ce23-2726-405d-817b-0d461f206737">            <a href="https://www.bestbuy.com/product/jbl-charge-6-portable-wireless-speaker-2025-black/J7LXFW3J5H" data-model-name="JBL Charge 6 Portable Wireless Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/fNc3HowqkmrigZci7YeZP9.jpg" alt="JBL Charge 6 Portable Wireless Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>JBL</div>                                        <div class="featured__title">JBL Charge 6 Portable Wireless Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="6394d793-4f9d-4169-b8e9-f4287d157704">            <a href="https://www.bestbuy.com/product/sony-ult-field-7-wireless-speaker-black/J7XSRH5SLS" data-model-name="Sony ULT Field 7 Wireless Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/jcBiuei57zrGSDzAbcJr2S.jpg" alt="Sony ULT Field 7 Wireless Speaker."></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">Sony ULT Field 7 Wireless Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="0fd1810e-fa9f-4d2b-b99e-69fd694e6858">            <a href="https://www.bestbuy.com/product/klipsch-miami-music-city-bluetooth-party-speaker-black/JX44FC55Y2" data-model-name="Klipsch Miami Music City Bluetooth Party Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/BzW3r6PT8QwtpdanA5diAW.jpg" alt="Klipsch Miami Music City Bluetooth Party Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Klipsch</div>                                        <div class="featured__title">Klipsch Miami Music City Bluetooth Party Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="37fe2c4e-9a04-4d51-a317-dbe53dc403cc">            <a href="https://www.bestbuy.com/product/jbl-party-box-710-portable-party-speaker-2024-black/J7LXFW26JQ" data-model-name="JBL Party Box 710 Party Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/Z5YFfJTwNSdswvDE4HJVth.jpg" alt="JBL Party Box 710 Party Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>JBL</div>                                        <div class="featured__title">JBL Party Box 710 Party Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="790534ee-31fc-415c-9f5c-cd0a02a81e31">            <a href="https://www.bestbuy.com/product/sony-ult-tower-9-wireless-party-speaker-srsult900-black/J7XSRH5HJF" data-model-name="Sony ULT Tower 9 Party Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/2Jg8KepmdYjZu8bGsVDoJn.jpg" alt="Sony ULT Tower 9 Party Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">Sony ULT Tower 9 Party Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="fd814dd7-86e9-4a5f-88c0-ee3097d40a6c">            <a href="https://www.bestbuy.com/product/klipsch-new-york-music-city-bluetooth-party-speaker-black/JX44FC55SQ" data-model-name="Klipsch New York Music City Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/tdvFuabkWw8cNh4ViovHHT.jpg" alt="Klipsch New York Music City Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Klipsch</div>                                        <div class="featured__title">Klipsch New York Music City Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="8ca1621c-d3c3-4cfa-9c37-04fa3939224e">            <a href="https://www.bestbuy.com/product/sony-ult-tower-10-party-speaker-with-wireless-microphone-black/J7XSRH5SY2" data-model-name="Sony ULT Tower 10 Party Speaker with Wireless Microphone" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/ccNEpLjSb4ZqGBPTSnBm5c.jpg" alt="Sony ULT Tower 10 Party Speaker with Wireless Microphone"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">Sony ULT Tower 10 Party Speaker with Wireless Microphone</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/best-buy-4th-of-july-sale-score-up-to-33-percent-off-these-9-bluetooth-speakers-from-sony-klipsch-jbl-and-more</link>
                                                                            <description>
                            <![CDATA[ Get the party started with one of these seriously good-sounding Bluetooth speakers – all now with healthy discounts. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">JPMnAfETWjyzJNKAaJBqbD</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/4838biTYLTZN6JA3uoH52E-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 30 Jun 2026 19:17:08 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                                    <dc:creator><![CDATA[ Will Salmon ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/HpRWP7Mh9ErgRGoTvZBbBJ.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Will Salmon is Tom&#039;s Guide&#039;s Collections Editor, curating advice on the best products to spend your money on. Will has been writing about tech and entertainment for almost 20 years, including stints as GamesRadar+&#039;s Streaming Editor, editing comics website Newsarama, and writing for lots and lots of magazines.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/4838biTYLTZN6JA3uoH52E-1280-80.jpg">
                                                            <media:credit><![CDATA[Sony, Anker, Klipsch, JBL / Edited by Gemini]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A selection of Bluetooth speakers.]]></media:description>                                                            <media:text><![CDATA[A selection of Bluetooth speakers.]]></media:text>
                                <media:title type="plain"><![CDATA[A selection of Bluetooth speakers.]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/4838biTYLTZN6JA3uoH52E-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The deals just keep coming! Yes, Amazon's Prime Day sales may be over, but Best Buy is already running a huge 4th of July sale that's offering lots of great bargains. We covered the <a href="https://www.tomsguide.com/tvs/4th-of-july-tv-deals-save-up-to-usd1-700-on-lg-samsung-and-more-at-best-buy">Best Buy TV sale</a> earlier and now we're rounding up the best Bluetooth speakers, which you can get right now with some pretty deep discounts.</p><p>Bluetooth speakers come in all shapes and sizes and have a variety of price points to match. We've started our selection with some good value Soundcore and Marshall offerings that are ideal for taking to the beach or lounging by the pool. We've then turned our attention to some feature-packed offerings from the likes of JBL before rounding out with some seriously high-end speakers from the likes of Sony and Klipsch. </p><p>For more amazing deals, make sure to check out our guide to the best <a href="https://www.tomsguide.com/sales-events/best-4th-of-july-sales-2026-walmart-home-depot-more">4th of July sales</a>.</p>        <div class="featured_product_block featured_block_hero" data-id="712f8dac-c812-4827-ac4f-3e4e108c1cbc">            <a href="https://www.bestbuy.com/product/soundcore-by-anker-boom-2-ipx7-waterproof-floatable-80w-bluetooth-speaker-with-subwoofer-bassup-2-0-custom-rgb-light-effects-black/JJ858R2J8J" data-model-name="Soundcore by Anker Boom 2 Waterproof Floatable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/SFBcdMYYSfhFFS4dFidjba.jpg" alt="Soundcore by Anker Boom 2 Waterproof Floatable Bluetooth Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Soundcore</div>                                        <div class="featured__title">Soundcore by Anker Boom 2 Waterproof Floatable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="72e8b1f0-9065-4207-897c-b2786e91ab90">            <a href="https://www.bestbuy.com/product/marshall-emberton-iii-portable-bluetooth-speaker-with-rugged-waterproof-dust-proof-wireless-design-black-brass/CFCR4HZWQ3" data-model-name="Marshall Emberton III Portable Bluetooth Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/pTDwusmBCM8F6nQVfBS4kj.jpg" alt="Marshall Emberton III Portable Bluetooth Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Marshall</div>                                        <div class="featured__title">Marshall Emberton III Portable Bluetooth Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="da87ce23-2726-405d-817b-0d461f206737">            <a href="https://www.bestbuy.com/product/jbl-charge-6-portable-wireless-speaker-2025-black/J7LXFW3J5H" data-model-name="JBL Charge 6 Portable Wireless Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/fNc3HowqkmrigZci7YeZP9.jpg" alt="JBL Charge 6 Portable Wireless Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>JBL</div>                                        <div class="featured__title">JBL Charge 6 Portable Wireless Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="6394d793-4f9d-4169-b8e9-f4287d157704">            <a href="https://www.bestbuy.com/product/sony-ult-field-7-wireless-speaker-black/J7XSRH5SLS" data-model-name="Sony ULT Field 7 Wireless Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/jcBiuei57zrGSDzAbcJr2S.jpg" alt="Sony ULT Field 7 Wireless Speaker."></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">Sony ULT Field 7 Wireless Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="0fd1810e-fa9f-4d2b-b99e-69fd694e6858">            <a href="https://www.bestbuy.com/product/klipsch-miami-music-city-bluetooth-party-speaker-black/JX44FC55Y2" data-model-name="Klipsch Miami Music City Bluetooth Party Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/BzW3r6PT8QwtpdanA5diAW.jpg" alt="Klipsch Miami Music City Bluetooth Party Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Klipsch</div>                                        <div class="featured__title">Klipsch Miami Music City Bluetooth Party Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="37fe2c4e-9a04-4d51-a317-dbe53dc403cc">            <a href="https://www.bestbuy.com/product/jbl-party-box-710-portable-party-speaker-2024-black/J7LXFW26JQ" data-model-name="JBL Party Box 710 Party Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/Z5YFfJTwNSdswvDE4HJVth.jpg" alt="JBL Party Box 710 Party Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>JBL</div>                                        <div class="featured__title">JBL Party Box 710 Party Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="790534ee-31fc-415c-9f5c-cd0a02a81e31">            <a href="https://www.bestbuy.com/product/sony-ult-tower-9-wireless-party-speaker-srsult900-black/J7XSRH5HJF" data-model-name="Sony ULT Tower 9 Party Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/2Jg8KepmdYjZu8bGsVDoJn.jpg" alt="Sony ULT Tower 9 Party Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">Sony ULT Tower 9 Party Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="fd814dd7-86e9-4a5f-88c0-ee3097d40a6c">            <a href="https://www.bestbuy.com/product/klipsch-new-york-music-city-bluetooth-party-speaker-black/JX44FC55SQ" data-model-name="Klipsch New York Music City Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/tdvFuabkWw8cNh4ViovHHT.jpg" alt="Klipsch New York Music City Speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Klipsch</div>                                        <div class="featured__title">Klipsch New York Music City Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="8ca1621c-d3c3-4cfa-9c37-04fa3939224e">            <a href="https://www.bestbuy.com/product/sony-ult-tower-10-party-speaker-with-wireless-microphone-black/J7XSRH5SY2" data-model-name="Sony ULT Tower 10 Party Speaker with Wireless Microphone" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:133.33%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/ccNEpLjSb4ZqGBPTSnBm5c.jpg" alt="Sony ULT Tower 10 Party Speaker with Wireless Microphone"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">Sony ULT Tower 10 Party Speaker with Wireless Microphone</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Ruark Audio Sabre-R review: Bookshelf speakers for grown-ups ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Sabre-R are a set of mid-range <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html"><u>bookshelf speakers</u></a> from family-owned British manufacturer Ruark Audio. In time-honored British fashion, Ruark started life in a leafy English suburb, with a middle-aged man (in this case, Brian O’Rourke) hand-making his own speakers in a shed.</p><p>Now owned by Brian’s son, Alan, the ethos at Ruark is (as it has been since the shed days) to make audio gear that the staff would want to use and own themselves. Products have to sound <em>and</em> look the part.</p><p>And that sums up the Sabre-R speakers perfectly. They combine excellent fidelity with Ruark’s typical understated elegance. There’s one major shortcoming, which is a lack of raw bass power in the very low range. But if you’re after a compact set of high-end speakers for your lounge, kitchen, turntable or whatever, these are worth serious consideration.</p><p>Find out more in my full Ruark Audio Sabre-R review.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-specs"><span>Ruark Audio Sabre-R review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.ruarkaudio.com/us/products/sabre-r-bookshelf-speakers" target="_blank" rel="nofollow"><u>$999</u></a> / <a href="https://www.richersounds.com/ruark-audio-sabre-r-charcoal/" target="_blank" rel="nofollow"><u>£699</u></a></p></td></tr><tr><td class="firstcol " ><p><strong>Speakers</strong></p></td><td  ><p>1.1-inch dome tweeter + 5-inch woofer</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>45Hz - 22kHz</p></td></tr><tr><td class="firstcol " ><p><strong>Impedance</strong></p></td><td  ><p>6 Ohm</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Passive, single- / bi-wire</p></td></tr><tr><td class="firstcol " ><p><strong>Weight (per speaker)</strong></p></td><td  ><p>11lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (per speaker)</strong></p></td><td  ><p>11.4 x 6.9 x 8.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Walnut; Charcoal</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-price-availability"><span>Ruark Audio Sabre-R review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5318px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="UYGSPMVBWNN5xgDtwgkzi7" name="Ruark Sabre-R_3" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/UYGSPMVBWNN5xgDtwgkzi7.jpg" mos="" align="middle" fullscreen="" width="5318" height="2991" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Ruark Sabre-R desktop speakers are available in charcoal or walnut colors and cost <a href="https://www.ruarkaudio.com/us/products/sabre-r-bookshelf-speakers" target="_blank" rel="nofollow"><u>$999 direct from Ruark</u></a>, as well as from specialist hi-fi retailers across the country. Ruark is still working on distributing its products more widely in the U.S., although you can find some of its range (not these) at Nordstrom. </p><p>In the U.K., where the brand is better established, the Sabre-R are much easier to find. You can snag a pair at <a href="https://www.richersounds.com/ruark-audio-sabre-r-charcoal/" target="_blank" rel="nofollow"><u>Richer Sounds</u></a>, <a href="https://www.sevenoakssoundandvision.co.uk/p-68445-ruark-audio-sabre-r-bookshelf-speakers.aspx" target="_blank" rel="nofollow"><u>Sevenoaks</u></a>, <a href="https://www.johnlewis.com/ruark-sabre-r-bookshelf-speakers-charcoal/charccoal/p112815407" target="_blank" rel="nofollow"><u>John Lewis</u></a>, or from <a href="https://www.ruarkaudio.com/products/sabre-r-bookshelf-speakers" target="_blank" rel="nofollow"><u>Ruark itself for £699</u></a>. </p><p>This obviously makes the Sabre-R a considered purchase, rather than an impulse one. They’re pricey, but premium hi-fi gear always is. And besides, in the wider context of high-end audio gear, which can happily reach into five- and six-digit price tags, $999 is still a fairly tame price.</p><p>If you’re on a tight budget, though, check out the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review"><u>Edifier R1280T</u></a>, which pack decent performance for under $200. For a little more, the $499 <a href="https://www.tomsguide.com/audio/speakers/edifier-s880db-mkii-review"><u>Edifier S880DB MKII</u></a> offer ‘premium-brand-worthy’ sound; while the <a href="https://www.tomsguide.com/audio/kanto-ren-review"><u>Kanto Ora Ren</u></a> cost $699 and offer plentiful bass performance.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-design"><span>Ruark Audio Sabre-R review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3511px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="EQgumkV8wQb4ZP2YeyqqK8" name="Ruark Sabre-R_1" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/EQgumkV8wQb4ZP2YeyqqK8.jpg" mos="" align="middle" fullscreen="" width="3511" height="1976" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Sabre-R are, in my opinion, absolutely stunning. As you can imagine given my job, I bring home <em>a lot</em> of tech. While my house is generally cluttered with all kinds of gizmos and gadgets, very little passes the bar to get into our lounge. But the Sabre-R sailed straight through, with a gleeful “what are <em>those</em>?!” from my wife.</p><p>That’s down to the Sabre-R’s typical Ruark elegance. Clean lines, high-quality materials, dark wood finishes. It’s timeless. These are speakers you want in your home — an accessory to any grown-up’s decor. They’re available in either walnut or charcoal variants. I have the charcoal model, which I prefer — they look a little sleeker and more understated, yet you still get a dash of that premium-feeling wood finish on the lower cabinet frontage.</p><p>As bookshelf speakers, the Sabre-Rs are fairly compact, measuring 11.4 x 6.9 x 8.5 inches. However, you obviously need to factor in an amplifier (ideally the Ruark R610 music console, which the Sabre-Rs are intended to be paired with) and its power cables, plus speaker cables. The result of all that was everything looking a little messy on my actual bookshelf. Instead, I first stuck them on my lounge TV stand, before moving them to my kitchen/dining room sideboard — I tend to spend more time in the kitchen, and that’s where we tend to host people, so this was the ideal spot for maximum usage.</p><h3 id="connectivity-8">Connectivity</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5238px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Dhg5cqfikSKVRNqGEjGMV7" name="Ruark Sabre-R_5" alt="The Ruark Audio Sabre-R bookshelf speakers rear ports" src="https://cdn.mos.cms.futurecdn.net/Dhg5cqfikSKVRNqGEjGMV7.jpg" mos="" align="middle" fullscreen="" width="5238" height="2946" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>There isn’t much to say about the Sabre-R connectivity-wise. These are passive speakers so it’s fairly straightforward. You’ve got a set of binding posts on each speaker, with sockets for single-wire connection and removable bridges for bi-wire.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-sound-quality"><span>Ruark Audio Sabre-R review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6240px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="5QH9i3QKCMZPqwXHtmxno7" name="Ruark Sabre-R_2" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/5QH9i3QKCMZPqwXHtmxno7.jpg" mos="" align="middle" fullscreen="" width="6240" height="3512" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Ruark Sabre-R’s sound quality is, for the most part, excellent. During testing, I streamed some music at CD quality via AirPlay using my <a href="https://www.tomsguide.com/reviews/denon-avr-x4800h"><u>Denon AVR-X4800H</u></a>. However, I mostly streamed in hi-res via the <a href="https://www.nordstrom.com/s/r610-music-console/8749905" target="_blank" rel="nofollow"><u>Ruark R610 music console</u></a> and <a href="https://www.tomsguide.com/audio/qobuz-review"><u>Qobuz</u></a> Connect. </p><p>The Sabre-R offer a typically warm, hi-fi profile, with deftly controlled bass — it’s everywhere you want it, nowhere you don’t. In electronica, where one wants lots of bass, it’s there: the dense, pulsating bassline of Burn Water’s <em>Ikigai</em> sounded gloriously thick, with plenty of power and no distortion. Yet the live-recorded basslines in folk, blues, rock etc, never felt overwhelming or boomy — Odetta’s <em>Hit or Miss</em> being a prime example.</p><iframe allow="" height="390" width="380" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/59666746?zone=GB-en"></iframe><p>The only issue I had in the low end was a lack of extension in the sub-bass region, as the Sabre-R's frequency response bottoms out at 45Hz. It’s not a problem for most songs, but makes a difference in those tracks where you’re supposed to feel something as well as hear it. Through the right speakers, for instance, the last 20 seconds of SOHN’s <em>Antigravity</em> are experienced more in the solar plexus than through the ear canals. I had to crank the Sabre-Rs up nearly to their max to get anywhere close, but that came with a shedload of distortion.</p><p>I’ve read that the Sabre-R’s very low end response improves over time as the drivers wear in. However, if you want that raw low down oomph straight outta the box, I’d be looking to the Cambridge Audio L/R X instead, which cover down to 35Hz and have a downfiring second woofer for plenty of addictive subbiness.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5898px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="K7SRSXf6Sy6sk3N3ofRuJ8" name="Ruark Sabre-R_4" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/K7SRSXf6Sy6sk3N3ofRuJ8.jpg" mos="" align="middle" fullscreen="" width="5898" height="3317" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>But anyway, back to the good stuff. Mids are energetic and textured on the Sabre-Rs, while treble is expansive and clean. The result is especially lively, immediate-sounding vocals, whatever the genre, whatever the timbre. Even in busier productions, like The Who’s <em>Eminence Front</em>, or with softer, breathier voices like Phoebe Bridgers’ in <em>Kyoto</em>, vocals were given the space and prominence they needed. Obviously, this is a boon for powerful voices too: John Fogerty’s righteous (and essentially shouted) indignance in CCR’s <em>Fortunate Son</em>; Phil Collins’ gleeful post-breakup gloating in <em>I Don’t Wanna Know</em>; Chris Stapleton’s reverbed, melancholic wails in <em>Death Row</em>. I’m not usually a vocals man, but I damn well became one with the Sabre-Rs.</p><p>Guitars sound fantastic, too, and there’s plenty of instrument separation, again thanks to that well-defined mid range. Melvins’ <em>Honey Bucket</em> and Metallica’s <em>Seek & Destroy</em> both use simultaneous distorted riffs, which I could tell apart from one another with ease. Detail never feels lost, either, thanks to that expansive treble. I could hear the softest percussion in Mk.Gee’s <em>Dream Police</em>, and the gentle, ethereal backing vocals in Burn Water's <em>Ikigai</em>.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-verdict"><span>Ruark Audio Sabre-R review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6240px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="p8goaxn6ErRpXkDsfo57Q9" name="Ruark Sabre-R 16-9" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/p8goaxn6ErRpXkDsfo57Q9.jpg" mos="" align="middle" fullscreen="" width="6240" height="3512" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>All in all, if you’re looking for a set of speakers that’ll do everything, from detailed high fidelity listening through to room-filling bass at parties, the Sabre-Rs probably ain’t it — they just lack that pure sub-bass power. Again, I’d spend a little more on the Cambridge L/R X, or drop similar money into the Kanto TUK + Sub 6V — both will get that raw, room-filling subbiness that vibrates up through your body. </p><p>The Sabre-Rs aren’t really supposed to be that, though. They’re a little more refined than that — a little more mature, if I may be so bold. If you don’t care about shaking windows and just want a set of speakers for relaxed detail listening, with that smooth hi-fi sound but no overpowering low end, well, these are for you. They’re perfect for kicking back in your lounge chair on a Sunday afternoon to some laidback vinyl playback, or soundtracking small gatherings with audio-appreciative friends. And man, they’re pretty.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/speakers/ruark-audio-sabre-r-review</link>
                                                                            <description>
                            <![CDATA[ Beautiful, warm hi-fi sound and looks to die for… what more could you want? ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">xQ4ziGUZq6pXisqXByAD5R</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/p8goaxn6ErRpXkDsfo57Q9-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 30 Jun 2026 12:11:20 +0000</pubDate>                                                                                                                                <updated>Tue, 30 Jun 2026 12:16:47 +0000</updated>
                                                                                                                                            <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ peter.wolinski@futurenet.com (Peter Wolinski) ]]></author>                    <dc:creator><![CDATA[ Peter Wolinski ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/stgPfXWY7ukw8J8rfC7vjg.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Peter is a Senior Editor at Tom&#039;s Guide, heading up the site&#039;s Reviews team and Cameras section. Having built gaming PCs since he was 10 (that&#039;s a while ago now) he&#039;s a bit of a nerd about components and hardware. He&#039;s also been an iPhone user since the classic iPhone 4, and a Mac user for well over a decade. Experienced in using and testing all kinds of technology — from phones through to tablets, computers, games consoles, cameras and smart home tech — helping people find the best tech for them (at the best prices) is what Peter does best. A photographer since he bought his first camera (a Fujifilm) in 2015, Peter was previously an Editor for Canon-Europe.com. He then edited the Cameras and How To sections of Tom&#039;s Guide. When he&#039;s not crafting helpful, in-depth reviews, Peter can usually be found out and about honing his architectural photography skills, riding his motorcycle around Welsh mountain roads, telling everyone about his two greyhounds, squeezing a few extra FPS out of PC games or perfecting his espresso shots.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/p8goaxn6ErRpXkDsfo57Q9-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The Ruark Audio Sabre-R bookshelf speakers in charcoal]]></media:description>                                                            <media:text><![CDATA[The Ruark Audio Sabre-R bookshelf speakers in charcoal]]></media:text>
                                <media:title type="plain"><![CDATA[The Ruark Audio Sabre-R bookshelf speakers in charcoal]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/p8goaxn6ErRpXkDsfo57Q9-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Sabre-R are a set of mid-range <a href="https://www.tomsguide.com/us/best-computer-speakers,review-6355.html"><u>bookshelf speakers</u></a> from family-owned British manufacturer Ruark Audio. In time-honored British fashion, Ruark started life in a leafy English suburb, with a middle-aged man (in this case, Brian O’Rourke) hand-making his own speakers in a shed.</p><p>Now owned by Brian’s son, Alan, the ethos at Ruark is (as it has been since the shed days) to make audio gear that the staff would want to use and own themselves. Products have to sound <em>and</em> look the part.</p><p>And that sums up the Sabre-R speakers perfectly. They combine excellent fidelity with Ruark’s typical understated elegance. There’s one major shortcoming, which is a lack of raw bass power in the very low range. But if you’re after a compact set of high-end speakers for your lounge, kitchen, turntable or whatever, these are worth serious consideration.</p><p>Find out more in my full Ruark Audio Sabre-R review.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-specs"><span>Ruark Audio Sabre-R review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.ruarkaudio.com/us/products/sabre-r-bookshelf-speakers" target="_blank" rel="nofollow"><u>$999</u></a> / <a href="https://www.richersounds.com/ruark-audio-sabre-r-charcoal/" target="_blank" rel="nofollow"><u>£699</u></a></p></td></tr><tr><td class="firstcol " ><p><strong>Speakers</strong></p></td><td  ><p>1.1-inch dome tweeter + 5-inch woofer</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>45Hz - 22kHz</p></td></tr><tr><td class="firstcol " ><p><strong>Impedance</strong></p></td><td  ><p>6 Ohm</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Passive, single- / bi-wire</p></td></tr><tr><td class="firstcol " ><p><strong>Weight (per speaker)</strong></p></td><td  ><p>11lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (per speaker)</strong></p></td><td  ><p>11.4 x 6.9 x 8.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Walnut; Charcoal</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-price-availability"><span>Ruark Audio Sabre-R review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5318px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="UYGSPMVBWNN5xgDtwgkzi7" name="Ruark Sabre-R_3" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/UYGSPMVBWNN5xgDtwgkzi7.jpg" mos="" align="middle" fullscreen="" width="5318" height="2991" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Ruark Sabre-R desktop speakers are available in charcoal or walnut colors and cost <a href="https://www.ruarkaudio.com/us/products/sabre-r-bookshelf-speakers" target="_blank" rel="nofollow"><u>$999 direct from Ruark</u></a>, as well as from specialist hi-fi retailers across the country. Ruark is still working on distributing its products more widely in the U.S., although you can find some of its range (not these) at Nordstrom. </p><p>In the U.K., where the brand is better established, the Sabre-R are much easier to find. You can snag a pair at <a href="https://www.richersounds.com/ruark-audio-sabre-r-charcoal/" target="_blank" rel="nofollow"><u>Richer Sounds</u></a>, <a href="https://www.sevenoakssoundandvision.co.uk/p-68445-ruark-audio-sabre-r-bookshelf-speakers.aspx" target="_blank" rel="nofollow"><u>Sevenoaks</u></a>, <a href="https://www.johnlewis.com/ruark-sabre-r-bookshelf-speakers-charcoal/charccoal/p112815407" target="_blank" rel="nofollow"><u>John Lewis</u></a>, or from <a href="https://www.ruarkaudio.com/products/sabre-r-bookshelf-speakers" target="_blank" rel="nofollow"><u>Ruark itself for £699</u></a>. </p><p>This obviously makes the Sabre-R a considered purchase, rather than an impulse one. They’re pricey, but premium hi-fi gear always is. And besides, in the wider context of high-end audio gear, which can happily reach into five- and six-digit price tags, $999 is still a fairly tame price.</p><p>If you’re on a tight budget, though, check out the <a href="https://www.tomsguide.com/audio/speakers/edifier-r1280t-review"><u>Edifier R1280T</u></a>, which pack decent performance for under $200. For a little more, the $499 <a href="https://www.tomsguide.com/audio/speakers/edifier-s880db-mkii-review"><u>Edifier S880DB MKII</u></a> offer ‘premium-brand-worthy’ sound; while the <a href="https://www.tomsguide.com/audio/kanto-ren-review"><u>Kanto Ora Ren</u></a> cost $699 and offer plentiful bass performance.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-design"><span>Ruark Audio Sabre-R review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3511px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="EQgumkV8wQb4ZP2YeyqqK8" name="Ruark Sabre-R_1" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/EQgumkV8wQb4ZP2YeyqqK8.jpg" mos="" align="middle" fullscreen="" width="3511" height="1976" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Sabre-R are, in my opinion, absolutely stunning. As you can imagine given my job, I bring home <em>a lot</em> of tech. While my house is generally cluttered with all kinds of gizmos and gadgets, very little passes the bar to get into our lounge. But the Sabre-R sailed straight through, with a gleeful “what are <em>those</em>?!” from my wife.</p><p>That’s down to the Sabre-R’s typical Ruark elegance. Clean lines, high-quality materials, dark wood finishes. It’s timeless. These are speakers you want in your home — an accessory to any grown-up’s decor. They’re available in either walnut or charcoal variants. I have the charcoal model, which I prefer — they look a little sleeker and more understated, yet you still get a dash of that premium-feeling wood finish on the lower cabinet frontage.</p><p>As bookshelf speakers, the Sabre-Rs are fairly compact, measuring 11.4 x 6.9 x 8.5 inches. However, you obviously need to factor in an amplifier (ideally the Ruark R610 music console, which the Sabre-Rs are intended to be paired with) and its power cables, plus speaker cables. The result of all that was everything looking a little messy on my actual bookshelf. Instead, I first stuck them on my lounge TV stand, before moving them to my kitchen/dining room sideboard — I tend to spend more time in the kitchen, and that’s where we tend to host people, so this was the ideal spot for maximum usage.</p><h3 id="connectivity-8">Connectivity</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5238px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Dhg5cqfikSKVRNqGEjGMV7" name="Ruark Sabre-R_5" alt="The Ruark Audio Sabre-R bookshelf speakers rear ports" src="https://cdn.mos.cms.futurecdn.net/Dhg5cqfikSKVRNqGEjGMV7.jpg" mos="" align="middle" fullscreen="" width="5238" height="2946" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>There isn’t much to say about the Sabre-R connectivity-wise. These are passive speakers so it’s fairly straightforward. You’ve got a set of binding posts on each speaker, with sockets for single-wire connection and removable bridges for bi-wire.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-sound-quality"><span>Ruark Audio Sabre-R review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6240px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="5QH9i3QKCMZPqwXHtmxno7" name="Ruark Sabre-R_2" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/5QH9i3QKCMZPqwXHtmxno7.jpg" mos="" align="middle" fullscreen="" width="6240" height="3512" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Ruark Sabre-R’s sound quality is, for the most part, excellent. During testing, I streamed some music at CD quality via AirPlay using my <a href="https://www.tomsguide.com/reviews/denon-avr-x4800h"><u>Denon AVR-X4800H</u></a>. However, I mostly streamed in hi-res via the <a href="https://www.nordstrom.com/s/r610-music-console/8749905" target="_blank" rel="nofollow"><u>Ruark R610 music console</u></a> and <a href="https://www.tomsguide.com/audio/qobuz-review"><u>Qobuz</u></a> Connect. </p><p>The Sabre-R offer a typically warm, hi-fi profile, with deftly controlled bass — it’s everywhere you want it, nowhere you don’t. In electronica, where one wants lots of bass, it’s there: the dense, pulsating bassline of Burn Water’s <em>Ikigai</em> sounded gloriously thick, with plenty of power and no distortion. Yet the live-recorded basslines in folk, blues, rock etc, never felt overwhelming or boomy — Odetta’s <em>Hit or Miss</em> being a prime example.</p><iframe allow="" height="390" width="380" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/59666746?zone=GB-en"></iframe><p>The only issue I had in the low end was a lack of extension in the sub-bass region, as the Sabre-R's frequency response bottoms out at 45Hz. It’s not a problem for most songs, but makes a difference in those tracks where you’re supposed to feel something as well as hear it. Through the right speakers, for instance, the last 20 seconds of SOHN’s <em>Antigravity</em> are experienced more in the solar plexus than through the ear canals. I had to crank the Sabre-Rs up nearly to their max to get anywhere close, but that came with a shedload of distortion.</p><p>I’ve read that the Sabre-R’s very low end response improves over time as the drivers wear in. However, if you want that raw low down oomph straight outta the box, I’d be looking to the Cambridge Audio L/R X instead, which cover down to 35Hz and have a downfiring second woofer for plenty of addictive subbiness.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5898px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="K7SRSXf6Sy6sk3N3ofRuJ8" name="Ruark Sabre-R_4" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/K7SRSXf6Sy6sk3N3ofRuJ8.jpg" mos="" align="middle" fullscreen="" width="5898" height="3317" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>But anyway, back to the good stuff. Mids are energetic and textured on the Sabre-Rs, while treble is expansive and clean. The result is especially lively, immediate-sounding vocals, whatever the genre, whatever the timbre. Even in busier productions, like The Who’s <em>Eminence Front</em>, or with softer, breathier voices like Phoebe Bridgers’ in <em>Kyoto</em>, vocals were given the space and prominence they needed. Obviously, this is a boon for powerful voices too: John Fogerty’s righteous (and essentially shouted) indignance in CCR’s <em>Fortunate Son</em>; Phil Collins’ gleeful post-breakup gloating in <em>I Don’t Wanna Know</em>; Chris Stapleton’s reverbed, melancholic wails in <em>Death Row</em>. I’m not usually a vocals man, but I damn well became one with the Sabre-Rs.</p><p>Guitars sound fantastic, too, and there’s plenty of instrument separation, again thanks to that well-defined mid range. Melvins’ <em>Honey Bucket</em> and Metallica’s <em>Seek & Destroy</em> both use simultaneous distorted riffs, which I could tell apart from one another with ease. Detail never feels lost, either, thanks to that expansive treble. I could hear the softest percussion in Mk.Gee’s <em>Dream Police</em>, and the gentle, ethereal backing vocals in Burn Water's <em>Ikigai</em>.</p><h2 class="article-body__section" id="section-ruark-audio-sabre-r-review-verdict"><span>Ruark Audio Sabre-R review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6240px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="p8goaxn6ErRpXkDsfo57Q9" name="Ruark Sabre-R 16-9" alt="The Ruark Audio Sabre-R bookshelf speakers in charcoal" src="https://cdn.mos.cms.futurecdn.net/p8goaxn6ErRpXkDsfo57Q9.jpg" mos="" align="middle" fullscreen="" width="6240" height="3512" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>All in all, if you’re looking for a set of speakers that’ll do everything, from detailed high fidelity listening through to room-filling bass at parties, the Sabre-Rs probably ain’t it — they just lack that pure sub-bass power. Again, I’d spend a little more on the Cambridge L/R X, or drop similar money into the Kanto TUK + Sub 6V — both will get that raw, room-filling subbiness that vibrates up through your body. </p><p>The Sabre-Rs aren’t really supposed to be that, though. They’re a little more refined than that — a little more mature, if I may be so bold. If you don’t care about shaking windows and just want a set of speakers for relaxed detail listening, with that smooth hi-fi sound but no overpowering low end, well, these are for you. They’re perfect for kicking back in your lounge chair on a Sunday afternoon to some laidback vinyl playback, or soundtracking small gatherings with audio-appreciative friends. And man, they’re pretty.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ All my family members want to steal this tiny Bluetooth speaker with huge sound from me, and it's only $47 for Prime Day ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I recently got my hands on the Tribit Stormbox Micro 3. I popped it out of the box, charged it up and switched my music on. Not only did it sound amazing, I’ve been using it for weeks since and still haven’t had to recharge.</p><p>I shouldn’t have been surprised, because Tribit consistently makes some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a>. And since it’s <a href="https://www.tomsguide.com/live/news/last-minute-prime-day-deals-picked-by-editors-2026">Prime Day</a> right now, you can score a saving on it that makes it an even better value.</p><div class="product"><a data-dimension112="140a926f-a639-4594-bf68-d05a6277ce82" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$47" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="LDHzrLzgPGmoS2J4wae5eE" name="tribit-stormbox-micro-3-portable-bluetoo-436aec5e-06de-4963-aa5c-3abc29a65587.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/LDHzrLzgPGmoS2J4wae5eE.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I love my Tribit Stormbox Micro 3. This palm-sized speaker brings huge sound, with customizable EQ, 24 hours of battery life on a charge and easy-to-use controls. It’s also great for taking outdoors with you thanks to its IP68 waterproof rating and drop protection. Best of all — it’s magnetic, so you can mount it on any magnetic surface and listen hands-free.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="140a926f-a639-4594-bf68-d05a6277ce82" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$47">View Deal</a></p></div><p>I know Bluetooth speakers aren’t a new concept, but I’m the kind of person who always uses my phone’s speaker for music. I wasn’t expecting to feel such a real difference from a device that’s not much larger than my phone and costs about twenty times less money.</p><p>When I was testing it out, I was blasting tunes so loud that they could be heard all through my house. And probably down the street, too. It brought a new dimension to the tracks I was listening to and a truly 3D-feeling soundscape. My soundtrack of choice the first time I used it was from Pikmin via the Nintendo Music app, and the naturalistic environmental sounds surrounding my ears from every direction made me feel like I was out experiencing the world right alongside the bite-sized creatures from the video game.</p><p>It’s rated for 24 hours of battery life. I’ve been using it for an hour or so a day for weeks since I got it, and the battery still hasn’t died on me. And reminder — this tiny speaker measures just about the same size as the palm of my hand.</p><h2 id="a-magnetic-design-literally">A magnetic design (literally)</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4363px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="mpYmpr4wAMUdFoxsr362Cg" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3" src="https://cdn.mos.cms.futurecdn.net/mpYmpr4wAMUdFoxsr362Cg.jpg" mos="" align="middle" fullscreen="" width="4363" height="2454" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of my favorite features about the Tribit Stormbox Micro 3 is that it’s magnetic. I’m always short on desk space, so I struggle to fit even this tiny speaker amongst my other stuff. But thanks to this speaker, I discovered that my desk has magnetic metal legs. Boom, better music and less clutter on my desktop all in one. It’s also great that I can stick it on my fridge door while I’m cooking in the kitchen. And if you’re outdoorsy, you could also mount it on your bike’s handlebars, or attach it to your backpack using the rubber strap.</p><p>I’m not the only person at Tom’s Guide who loves this speaker. My colleague Ashley Thieme wrote about it in her <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit Stormbox Micro 3 review</a> and had a ton of praise for its strong, bassy sound. I’m not too bothered about tweaking EQ for my music (sorry, audiophiles,) but take note that it is possible with this speaker. Use the Tribit app and you can tweak the sound exactly to your preferences.</p><p>The Tribit Stormbox Micro 3 has impressed me so much that I’d recommend it to just about anyone. In fact, multiple members of my family have already said they’re going to steal it off of me. So I guess I’ll either have to keep my speaker under lock and key or pick up a few more of them to hand out in the coming weeks.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/all-my-family-members-want-to-steal-this-tiny-bluetooth-speaker-with-huge-sound-from-me-and-its-only-usd47-for-prime-day</link>
                                                                            <description>
                            <![CDATA[ The Tribit Stormbox Micro 3 is a tiny powerhouse of a Bluetooth speaker; I can’t believe how good it is for the price. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">fHnPYXPyeNxDgFV9EAjLwN</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/ebuyeydkrX8PqiaHG6cTDN-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 26 Jun 2026 12:21:25 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ millie.daviswilliams@futurenet.com (Millie Davis-Williams) ]]></author>                    <dc:creator><![CDATA[ Millie Davis-Williams ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/WU6pa7hKuxs5ScCoqcXYWA.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Millie is a Deals Editor at Tom&#039;s Guide specializing in tech deals content. Based in London, Millie has spent a lifetime stubbornly refusing to pay full price on any possible purchase. She has years of searching for the best discounts on the latest and greatest phones, TVs and gadgets under her belt, and knows how to sort the best deals from the rest. Helping readers save on the products they want is her mission, whether it’s an Apple Watch or a new tablet.&lt;/p&gt;&lt;p&gt;Since working at Tom’s Guide, Millie has covered retail events like Prime Day, Black Friday and Cyber Monday extensively, meaning she knows the products readers are looking for and the best times of year to hit “purchase.”&lt;/p&gt;&lt;p&gt;When she’s not looking for discounts, Millie covers news and writes how-to articles about everything from phones, streaming devices, and headphones to apps and video games.&lt;/p&gt;&lt;p&gt;In her spare time, Millie enjoys reading, gaming on her Nintendo Switch and creating digital art. She’s also a fan of watching Disney movies and collecting plush (the latter having totally taken over her bedroom.)&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/ebuyeydkrX8PqiaHG6cTDN-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Tribit Stormbox Micro 3 Bluetooth speaker in hand]]></media:description>                                                            <media:text><![CDATA[Tribit Stormbox Micro 3 Bluetooth speaker in hand]]></media:text>
                                <media:title type="plain"><![CDATA[Tribit Stormbox Micro 3 Bluetooth speaker in hand]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/ebuyeydkrX8PqiaHG6cTDN-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I recently got my hands on the Tribit Stormbox Micro 3. I popped it out of the box, charged it up and switched my music on. Not only did it sound amazing, I’ve been using it for weeks since and still haven’t had to recharge.</p><p>I shouldn’t have been surprised, because Tribit consistently makes some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a>. And since it’s <a href="https://www.tomsguide.com/live/news/last-minute-prime-day-deals-picked-by-editors-2026">Prime Day</a> right now, you can score a saving on it that makes it an even better value.</p><div class="product"><a data-dimension112="140a926f-a639-4594-bf68-d05a6277ce82" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$47" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="LDHzrLzgPGmoS2J4wae5eE" name="tribit-stormbox-micro-3-portable-bluetoo-436aec5e-06de-4963-aa5c-3abc29a65587.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/LDHzrLzgPGmoS2J4wae5eE.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I love my Tribit Stormbox Micro 3. This palm-sized speaker brings huge sound, with customizable EQ, 24 hours of battery life on a charge and easy-to-use controls. It’s also great for taking outdoors with you thanks to its IP68 waterproof rating and drop protection. Best of all — it’s magnetic, so you can mount it on any magnetic surface and listen hands-free.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="140a926f-a639-4594-bf68-d05a6277ce82" data-action="Deal Block" data-label="Stormbox Micro 3" data-dimension48="Stormbox Micro 3" data-dimension25="$47">View Deal</a></p></div><p>I know Bluetooth speakers aren’t a new concept, but I’m the kind of person who always uses my phone’s speaker for music. I wasn’t expecting to feel such a real difference from a device that’s not much larger than my phone and costs about twenty times less money.</p><p>When I was testing it out, I was blasting tunes so loud that they could be heard all through my house. And probably down the street, too. It brought a new dimension to the tracks I was listening to and a truly 3D-feeling soundscape. My soundtrack of choice the first time I used it was from Pikmin via the Nintendo Music app, and the naturalistic environmental sounds surrounding my ears from every direction made me feel like I was out experiencing the world right alongside the bite-sized creatures from the video game.</p><p>It’s rated for 24 hours of battery life. I’ve been using it for an hour or so a day for weeks since I got it, and the battery still hasn’t died on me. And reminder — this tiny speaker measures just about the same size as the palm of my hand.</p><h2 id="a-magnetic-design-literally">A magnetic design (literally)</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4363px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="mpYmpr4wAMUdFoxsr362Cg" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3" src="https://cdn.mos.cms.futurecdn.net/mpYmpr4wAMUdFoxsr362Cg.jpg" mos="" align="middle" fullscreen="" width="4363" height="2454" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of my favorite features about the Tribit Stormbox Micro 3 is that it’s magnetic. I’m always short on desk space, so I struggle to fit even this tiny speaker amongst my other stuff. But thanks to this speaker, I discovered that my desk has magnetic metal legs. Boom, better music and less clutter on my desktop all in one. It’s also great that I can stick it on my fridge door while I’m cooking in the kitchen. And if you’re outdoorsy, you could also mount it on your bike’s handlebars, or attach it to your backpack using the rubber strap.</p><p>I’m not the only person at Tom’s Guide who loves this speaker. My colleague Ashley Thieme wrote about it in her <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit Stormbox Micro 3 review</a> and had a ton of praise for its strong, bassy sound. I’m not too bothered about tweaking EQ for my music (sorry, audiophiles,) but take note that it is possible with this speaker. Use the Tribit app and you can tweak the sound exactly to your preferences.</p><p>The Tribit Stormbox Micro 3 has impressed me so much that I’d recommend it to just about anyone. In fact, multiple members of my family have already said they’re going to steal it off of me. So I guess I’ll either have to keep my speaker under lock and key or pick up a few more of them to hand out in the coming weeks.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Google Home Speaker review: modest upgrades, but Gemini is the star ]]></title>
                                                                                                <dc:content><![CDATA[ <p>At long last, Google has refreshed its smart speaker lineup with the new Google Home Speaker; first announced in the fall of 2025, it took nearly nine months for the device to come to market, and replaces the six-year-old Google Nest Audio as the top-shelf smart speaker in the company’s portfolio.</p><p>Things have changed a lot since Google last released a smart speaker. During that time, AI got real big, so now the assistant powering the Google Home speaker can do a lot more than just turn on your lights.</p><p>So how does the Google Home Speaker stack up against the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a>? I’ve used it for a few days and compared it against its main rivals, the Apple HomePod mini and the Amazon Echo Dot Max, to find out.</p><h3 class="article-body__section" id="section-google-home-speaker-review-price-and-availability"><span>Google Home Speaker review: Price and availability</span></h3><p>The Google Home Speaker went on sale on June 24, 2026 for $99. You can get it in one of four colors: Jade (green), Berry (red), Porcelain (white) and Hazel (grayish black). However, the Jade and Berry colors are exclusive to the U.S.</p><p>The Google Home Speaker replaces the Google Nest Audio, and, for the moment, the Google Nest Mini, both of which are no longer for sale at Google’s online store.</p><h3 class="article-body__section" id="section-google-home-speaker-review-design"><span>Google Home Speaker review: Design</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dpBaqjHLfrdaQ5CCE7cn2W" name="Google Home speaker-6" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/dpBaqjHLfrdaQ5CCE7cn2W.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Google Home Speaker looks a bit like someone took the Apple HomePod mini and smushed it a little</p><p>There are two touch-sensitive buttons on the top of the speaker to control the volume. You can’t see the buttons, as they’re hidden by the fabric, but if you tap the top of the speaker, two LEDs turn on to show you their location. You can’t tell which is volume up or volume down by sight; you’ll have to tap them to find out which is which. Tapping the top of the speaker will also play/pause your music.</p><p>Google had similar hidden touch-sensitive buttons on its previous smart speakers with an equally mixed reception; the Mini’s buttons had to be deactivated because of their finicky nature. I just find it annoying that you have to give the speaker an extra tap.</p><p>Similar to the Echo Dot Max, the Google Home Speaker has a circular ring of LEDs which changes color based on the circumstance; for example, it turns white when Gemini is listening, multiple colors when it’s thinking, and orange when the microphone is off.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zCGPPi5GVbKBR7yA56YKVZ" name="Google Home speaker-3" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/zCGPPi5GVbKBR7yA56YKVZ.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Also on the base of the speaker is a physical switch to disable its microphones, which is a commendable feature. Less so is the nonremovable power cord; break it, and you’ll have to replace the whole device. </p><h3 class="article-body__section" id="section-google-home-speaker-review-sustainability"><span>Google Home Speaker review: Sustainability</span></h3><p>As with most companies’ claims, Google throws out some numbers which are ultimately a little vague. Google says the speaker and its adapter is “Made with at least 37% recycled materials based on product weight.” Whether that means the metal or plastic parts is unclear.</p><p>By comparison, Amazon says the <a href="https://sustainability.aboutamazon.com/devices_fact_sheet_echo_dot_max.pdf">Echo Dot Max is made from 23% recycled materials</a>; the fabric parts are made from 98% post-consumer recycled fabric, while the plastic is made from 41% post consumer recycled plastic.</p><p>The Apple HomePod mini uses <a href="https://www.apple.com/homepod-mini/specs/">99% recycled rare earth elements</a>, and 35% or more recycled plastic in multiple components.</p><h3 class="article-body__section" id="section-google-home-speaker-review-audio-performance"><span>Google Home Speaker review: Audio performance</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TkWLXUmvTyYDH9JpUrU2zd" name="Google Home speaker-2" alt="Google Home Speaker with Echo Dot Max and HomePod mini" src="https://cdn.mos.cms.futurecdn.net/TkWLXUmvTyYDH9JpUrU2zd.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Where the Nest Audio had a 75mm woofer and a 19mm tweeter, the Google Home Speaker has a single 58mm driver. I didn’t have the older Nest to compare the two side-by-side, but the tl;dr is that the Google Home Speaker performs well for its size. </p><p>I played a variety of tracks on the Google Home Speaker, as well as the HomePod mini and Echo Dot Max, and though there were some subtle differences between them, they weren’t all that disparate. </p><p>I might give a slight edge to the Echo Dot Max; it’s the only device that has two speakers (an 0.8-inch tweeter and 2.5-inch woofer) which helped midtones stand out a bit more than the Google Home and the HomePod mini, which has a single 2-inch driver </p><p>Mids sounded a bit compressed, such as Billy Joel’s voice in “Movin’ Out.” Joel’s voice sounded better and warmer on the Echo Dot Max, and there was also better bass. However, the HomePod mini sounded the best of the three. </p><p>Olivia Rodrigo’s “Drop Dead” sounded best on the Echo Dot Max, as it was able to better separate the mids and highs; the Google Home Speaker and HomePod mini mushed things a bit.</p><p>Not surprisingly, bass was just ok, I threw on some old-school Snoop Dog “Who am I (What’s My Name)?” and the groove was much better on the Echo Dot Max and the HomePod than Google’s speaker.  </p><h3 class="article-body__section" id="section-google-home-speaker-review-google-gemini"><span>Google Home Speaker review: Google Gemini</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="P9tEwT4b4it3uH4NMaAe8h" name="Google Home speaker-4" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/P9tEwT4b4it3uH4NMaAe8h.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The hardware may be new, but the bigger upgrade inside the Google Play Speaker is <a href="https://www.tomsguide.com/ai/google-gemini/google-gemini-everything-you-need-to-know">Gemini</a>, which is far more capable and powerful than Google Assistant. </p><p>Gemini for Home works with all of Google’s smart speakers and displays, but only the newer ones — Nest Audio, Nest mini (2nd Gen), Nest Hub Max and Nest Hub (2nd Gen) — will support the full Gemini for Home experience, as well as Gemini Live, where you can have a more natural, free-flowing conversation. </p><p>To test out Gemini on the Google Home Speaker, I asked it a series of questions, which it answered correctly, and was much more engaging than the old Assistant. For example, when I asked “Hey Google, what’s a good recipe for chicken wings?”, Gemini responded with a wide range of options, as well as certain things that it thought were popular. </p><p>I then asked Gemini to save the recipe, and it told me it created a note, but not where it was saved. I was able to find it in my Google Keep folder. </p><p>I also tested Gemini on sports, and here, it was a bit slower on the uptake. I asked ““Hey Google, did France win today?” and received the response “No, France did not play today,” although they had just defeated Iraq in the World Cup.</p><p>I then followed up with “Are you sure France didn’t play today?” and Gemini quickly corrected itself, gave me a summary of the game, and told me that <em>Les Bleus</em> secured a spot in the round of 32.</p><h3 class="article-body__section" id="section-google-home-speaker-review-subscription-costs"><span>Google Home Speaker review: Subscription costs</span></h3><p>You get Gemini for Home for free with the Google Home Speaker, but to make the most out of Gemini (and the speaker), you’ll need to sign up for a Google Home Premium subscription. Otherwise, you won’t get  conversational Gemini, among other things.</p><p>If you sign up for Google Home Premium Standard ($10/month or $100/year), you get the following:</p><ul><li><strong>Gemini Live (Get expert help, brainstorm ideas, and learn new topics)</strong></li><li><strong>Help me create (lets you describe to Gemini the smart home routine you want to create)</strong></li><li><strong>Sound detection (Smoke/carbon monoxide alarm and</strong><sup><strong> </strong></sup><strong>Glass breaking)</strong></li><li><strong>30 days of video event history</strong></li></ul><p>If you sign up for Google Home Premium Advanced ($20/month, $200/year), you get those features as well as these:</p><ul><li><strong>Search video history - Quickly search your camera history by just asking</strong></li><li><strong>Daily summaries - Ask about what you missed when you were away</strong></li><li><strong>60 days of video history</strong></li><li><strong>10 days of 24/7 video history</strong></li><li><strong>Descriptive notifications</strong></li><li><strong>Event descriptions</strong></li></ul><p>If you’re more invested in Gemini, you can also get the Standard plan as part of a <a href="https://gemini.google/subscriptions/">Google AI Pro subscription</a> ($20/month), and the Advanced plan with Google AI Ultra ($99/month).</p><h3 class="article-body__section" id="section-google-home-speaker-review-home-theater-features"><span>Google Home Speaker review: Home theater features</span></h3><p>Similar to its rival smart speakers, the Google Home Speaker works well with the Google TV Streamer to create a home theater experience. You can pair either a single speaker or two to the streaming device; if you pair two, you can also set up spatial audio. Dolby Atmos is supported, and the Google TV Streamer can also convert HD audio into a proprietary spatial audio format. </p><p>Amazon takes things a step further with its Echo Dot Max and Echo Studio speakers, as you can create a <a href="https://www.tomsguide.com/audio/smart-speakers/i-used-the-echo-dot-max-and-echo-studio-to-create-an-alexa-home-theater-heres-how-it-sounds">full surround sound experience</a> with up to five speakers. </p><h3 class="article-body__section" id="section-google-home-speaker-review-smart-home-functionality"><span>Google Home Speaker review: Smart Home functionality</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kLxn25x3gYoTuj8YjC3oum" name="Google Home speaker-7" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/kLxn25x3gYoTuj8YjC3oum.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>As a smart home device, the Google Home Speaker is less capable than its Apple and Amazon rivals. For example, the HomePod mini has temperature and humidity sensors; the Amazon Echo Dot Max can act as an <a href="https://www.tomsguide.com/features/the-echo-dot-now-works-as-an-eero-mesh-extender-just-how-good-is-it">eero mesh router extender</a> and has ambient light and temperature sensors, as well as presence detection. </p><p>Those other sensors make the other smart speakers far more functional. For example, you can create an Apple Home or Alexa routine to turn on your air conditioner if the temperature as measured by a speaker rises above a certain level, or if it senses someone entering the room.</p><h3 class="article-body__section" id="section-google-home-speaker-review-bottom-line"><span>Google Home Speaker review: Bottom line</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="taBNFcocc9giLoQnTgVVEA" name="Google Home speaker-5" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/taBNFcocc9giLoQnTgVVEA.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Smart speakers aren’t quite the commodity they once were; Like Amazon, Google has gone from releasing a new model every year to waiting more than half a decade between the Nest Audio and the Google Home Speaker.</p><p>And that’s fine. There’s only so much you can do, though both Amazon and Apple have done more with their smart speakers than Google in terms of embedding additional sensors to make their devices more integral to a smart home ecosystem.</p><p>As smart home assistants, both Alexa and Google are very capable, especially now that they’ve gained extra AI abilities. If you’ve already started using either smart assistant, it’s unlikely that you’re going to switch your smart home from Alexa to Google or vice versa.</p><p>At this point, a smart speaker is more or less a delivery mechanism for the smart assistant of your choice. If you already own a Google smart speaker and it’s working well, there’s no real reason to upgrade, unless you want to create a Google-centric home theater system.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/google-home-speaker-review</link>
                                                                            <description>
                            <![CDATA[ Aside from its design, the Google Home Speaker isn't a huge upgrade over the Nest Audio, and you can get all the same Gemini features on both devices. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">kqpFGZqfGzJxez3wyGEHoB</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/AfNUB2sJXddzNzhcp6uAWS-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Wed, 24 Jun 2026 13:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ mike.prospero@futurenet.com (Mike Prospero) ]]></author>                    <dc:creator><![CDATA[ Mike Prospero ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/6ZM8mX4UwccqDJTh9gLPqV.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Michael A. Prospero is the U.S. Editor-in-Chief for Tom’s Guide. He oversees all evergreen content and oversees the Homes, Smart Home, and Fitness/Wearables categories for the site. In his spare time, he also tests out the latest drones, electric scooters, and smart home gadgets, such as video doorbells. Before his tenure at Tom&#039;s Guide, he was the Reviews Editor for Laptop Magazine, a reporter at Fast Company, the Times of Trenton, and, many eons back, an intern at George magazine. He received his undergraduate degree from Boston College, where he worked on the campus newspaper The Heights, and then attended the Columbia University school of Journalism. When he’s not testing out the latest running watch, electric scooter, or skiing or training for a marathon, he’s probably using the latest sous vide machine, smoker, or pizza oven, to the delight — or chagrin — of his family.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/AfNUB2sJXddzNzhcp6uAWS-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Google Home Speaker]]></media:description>                                                            <media:text><![CDATA[Google Home Speaker]]></media:text>
                                <media:title type="plain"><![CDATA[Google Home Speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/AfNUB2sJXddzNzhcp6uAWS-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>At long last, Google has refreshed its smart speaker lineup with the new Google Home Speaker; first announced in the fall of 2025, it took nearly nine months for the device to come to market, and replaces the six-year-old Google Nest Audio as the top-shelf smart speaker in the company’s portfolio.</p><p>Things have changed a lot since Google last released a smart speaker. During that time, AI got real big, so now the assistant powering the Google Home speaker can do a lot more than just turn on your lights.</p><p>So how does the Google Home Speaker stack up against the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a>? I’ve used it for a few days and compared it against its main rivals, the Apple HomePod mini and the Amazon Echo Dot Max, to find out.</p><h3 class="article-body__section" id="section-google-home-speaker-review-price-and-availability"><span>Google Home Speaker review: Price and availability</span></h3><p>The Google Home Speaker went on sale on June 24, 2026 for $99. You can get it in one of four colors: Jade (green), Berry (red), Porcelain (white) and Hazel (grayish black). However, the Jade and Berry colors are exclusive to the U.S.</p><p>The Google Home Speaker replaces the Google Nest Audio, and, for the moment, the Google Nest Mini, both of which are no longer for sale at Google’s online store.</p><h3 class="article-body__section" id="section-google-home-speaker-review-design"><span>Google Home Speaker review: Design</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dpBaqjHLfrdaQ5CCE7cn2W" name="Google Home speaker-6" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/dpBaqjHLfrdaQ5CCE7cn2W.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Google Home Speaker looks a bit like someone took the Apple HomePod mini and smushed it a little</p><p>There are two touch-sensitive buttons on the top of the speaker to control the volume. You can’t see the buttons, as they’re hidden by the fabric, but if you tap the top of the speaker, two LEDs turn on to show you their location. You can’t tell which is volume up or volume down by sight; you’ll have to tap them to find out which is which. Tapping the top of the speaker will also play/pause your music.</p><p>Google had similar hidden touch-sensitive buttons on its previous smart speakers with an equally mixed reception; the Mini’s buttons had to be deactivated because of their finicky nature. I just find it annoying that you have to give the speaker an extra tap.</p><p>Similar to the Echo Dot Max, the Google Home Speaker has a circular ring of LEDs which changes color based on the circumstance; for example, it turns white when Gemini is listening, multiple colors when it’s thinking, and orange when the microphone is off.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zCGPPi5GVbKBR7yA56YKVZ" name="Google Home speaker-3" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/zCGPPi5GVbKBR7yA56YKVZ.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Also on the base of the speaker is a physical switch to disable its microphones, which is a commendable feature. Less so is the nonremovable power cord; break it, and you’ll have to replace the whole device. </p><h3 class="article-body__section" id="section-google-home-speaker-review-sustainability"><span>Google Home Speaker review: Sustainability</span></h3><p>As with most companies’ claims, Google throws out some numbers which are ultimately a little vague. Google says the speaker and its adapter is “Made with at least 37% recycled materials based on product weight.” Whether that means the metal or plastic parts is unclear.</p><p>By comparison, Amazon says the <a href="https://sustainability.aboutamazon.com/devices_fact_sheet_echo_dot_max.pdf">Echo Dot Max is made from 23% recycled materials</a>; the fabric parts are made from 98% post-consumer recycled fabric, while the plastic is made from 41% post consumer recycled plastic.</p><p>The Apple HomePod mini uses <a href="https://www.apple.com/homepod-mini/specs/">99% recycled rare earth elements</a>, and 35% or more recycled plastic in multiple components.</p><h3 class="article-body__section" id="section-google-home-speaker-review-audio-performance"><span>Google Home Speaker review: Audio performance</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TkWLXUmvTyYDH9JpUrU2zd" name="Google Home speaker-2" alt="Google Home Speaker with Echo Dot Max and HomePod mini" src="https://cdn.mos.cms.futurecdn.net/TkWLXUmvTyYDH9JpUrU2zd.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Where the Nest Audio had a 75mm woofer and a 19mm tweeter, the Google Home Speaker has a single 58mm driver. I didn’t have the older Nest to compare the two side-by-side, but the tl;dr is that the Google Home Speaker performs well for its size. </p><p>I played a variety of tracks on the Google Home Speaker, as well as the HomePod mini and Echo Dot Max, and though there were some subtle differences between them, they weren’t all that disparate. </p><p>I might give a slight edge to the Echo Dot Max; it’s the only device that has two speakers (an 0.8-inch tweeter and 2.5-inch woofer) which helped midtones stand out a bit more than the Google Home and the HomePod mini, which has a single 2-inch driver </p><p>Mids sounded a bit compressed, such as Billy Joel’s voice in “Movin’ Out.” Joel’s voice sounded better and warmer on the Echo Dot Max, and there was also better bass. However, the HomePod mini sounded the best of the three. </p><p>Olivia Rodrigo’s “Drop Dead” sounded best on the Echo Dot Max, as it was able to better separate the mids and highs; the Google Home Speaker and HomePod mini mushed things a bit.</p><p>Not surprisingly, bass was just ok, I threw on some old-school Snoop Dog “Who am I (What’s My Name)?” and the groove was much better on the Echo Dot Max and the HomePod than Google’s speaker.  </p><h3 class="article-body__section" id="section-google-home-speaker-review-google-gemini"><span>Google Home Speaker review: Google Gemini</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="P9tEwT4b4it3uH4NMaAe8h" name="Google Home speaker-4" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/P9tEwT4b4it3uH4NMaAe8h.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The hardware may be new, but the bigger upgrade inside the Google Play Speaker is <a href="https://www.tomsguide.com/ai/google-gemini/google-gemini-everything-you-need-to-know">Gemini</a>, which is far more capable and powerful than Google Assistant. </p><p>Gemini for Home works with all of Google’s smart speakers and displays, but only the newer ones — Nest Audio, Nest mini (2nd Gen), Nest Hub Max and Nest Hub (2nd Gen) — will support the full Gemini for Home experience, as well as Gemini Live, where you can have a more natural, free-flowing conversation. </p><p>To test out Gemini on the Google Home Speaker, I asked it a series of questions, which it answered correctly, and was much more engaging than the old Assistant. For example, when I asked “Hey Google, what’s a good recipe for chicken wings?”, Gemini responded with a wide range of options, as well as certain things that it thought were popular. </p><p>I then asked Gemini to save the recipe, and it told me it created a note, but not where it was saved. I was able to find it in my Google Keep folder. </p><p>I also tested Gemini on sports, and here, it was a bit slower on the uptake. I asked ““Hey Google, did France win today?” and received the response “No, France did not play today,” although they had just defeated Iraq in the World Cup.</p><p>I then followed up with “Are you sure France didn’t play today?” and Gemini quickly corrected itself, gave me a summary of the game, and told me that <em>Les Bleus</em> secured a spot in the round of 32.</p><h3 class="article-body__section" id="section-google-home-speaker-review-subscription-costs"><span>Google Home Speaker review: Subscription costs</span></h3><p>You get Gemini for Home for free with the Google Home Speaker, but to make the most out of Gemini (and the speaker), you’ll need to sign up for a Google Home Premium subscription. Otherwise, you won’t get  conversational Gemini, among other things.</p><p>If you sign up for Google Home Premium Standard ($10/month or $100/year), you get the following:</p><ul><li><strong>Gemini Live (Get expert help, brainstorm ideas, and learn new topics)</strong></li><li><strong>Help me create (lets you describe to Gemini the smart home routine you want to create)</strong></li><li><strong>Sound detection (Smoke/carbon monoxide alarm and</strong><sup><strong> </strong></sup><strong>Glass breaking)</strong></li><li><strong>30 days of video event history</strong></li></ul><p>If you sign up for Google Home Premium Advanced ($20/month, $200/year), you get those features as well as these:</p><ul><li><strong>Search video history - Quickly search your camera history by just asking</strong></li><li><strong>Daily summaries - Ask about what you missed when you were away</strong></li><li><strong>60 days of video history</strong></li><li><strong>10 days of 24/7 video history</strong></li><li><strong>Descriptive notifications</strong></li><li><strong>Event descriptions</strong></li></ul><p>If you’re more invested in Gemini, you can also get the Standard plan as part of a <a href="https://gemini.google/subscriptions/">Google AI Pro subscription</a> ($20/month), and the Advanced plan with Google AI Ultra ($99/month).</p><h3 class="article-body__section" id="section-google-home-speaker-review-home-theater-features"><span>Google Home Speaker review: Home theater features</span></h3><p>Similar to its rival smart speakers, the Google Home Speaker works well with the Google TV Streamer to create a home theater experience. You can pair either a single speaker or two to the streaming device; if you pair two, you can also set up spatial audio. Dolby Atmos is supported, and the Google TV Streamer can also convert HD audio into a proprietary spatial audio format. </p><p>Amazon takes things a step further with its Echo Dot Max and Echo Studio speakers, as you can create a <a href="https://www.tomsguide.com/audio/smart-speakers/i-used-the-echo-dot-max-and-echo-studio-to-create-an-alexa-home-theater-heres-how-it-sounds">full surround sound experience</a> with up to five speakers. </p><h3 class="article-body__section" id="section-google-home-speaker-review-smart-home-functionality"><span>Google Home Speaker review: Smart Home functionality</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kLxn25x3gYoTuj8YjC3oum" name="Google Home speaker-7" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/kLxn25x3gYoTuj8YjC3oum.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>As a smart home device, the Google Home Speaker is less capable than its Apple and Amazon rivals. For example, the HomePod mini has temperature and humidity sensors; the Amazon Echo Dot Max can act as an <a href="https://www.tomsguide.com/features/the-echo-dot-now-works-as-an-eero-mesh-extender-just-how-good-is-it">eero mesh router extender</a> and has ambient light and temperature sensors, as well as presence detection. </p><p>Those other sensors make the other smart speakers far more functional. For example, you can create an Apple Home or Alexa routine to turn on your air conditioner if the temperature as measured by a speaker rises above a certain level, or if it senses someone entering the room.</p><h3 class="article-body__section" id="section-google-home-speaker-review-bottom-line"><span>Google Home Speaker review: Bottom line</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="taBNFcocc9giLoQnTgVVEA" name="Google Home speaker-5" alt="Google Home Speaker" src="https://cdn.mos.cms.futurecdn.net/taBNFcocc9giLoQnTgVVEA.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Smart speakers aren’t quite the commodity they once were; Like Amazon, Google has gone from releasing a new model every year to waiting more than half a decade between the Nest Audio and the Google Home Speaker.</p><p>And that’s fine. There’s only so much you can do, though both Amazon and Apple have done more with their smart speakers than Google in terms of embedding additional sensors to make their devices more integral to a smart home ecosystem.</p><p>As smart home assistants, both Alexa and Google are very capable, especially now that they’ve gained extra AI abilities. If you’ve already started using either smart assistant, it’s unlikely that you’re going to switch your smart home from Alexa to Google or vice versa.</p><p>At this point, a smart speaker is more or less a delivery mechanism for the smart assistant of your choice. If you already own a Google smart speaker and it’s working well, there’s no real reason to upgrade, unless you want to create a Google-centric home theater system.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I test Bluetooth speakers for a living and I’ve found the best Prime Day speaker deals from Bose, JBL, Marshall and more, starting from just $27 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I don't know about you, but I love listening to music whenever possible. Though I usually use earbuds or headphones to do so, I like jamming to my favorite tunes by listening to them through the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> too, especially if I'm hanging out with my partner. Or soundtracking a party. Or performing karaoke in the shower. Or dancing to Tame Impala while I cook.</p><p>Technology has come leaps and bounds, and today, we're spoilt for choice when it comes to speakers. With so many choices, where do you even start? Luckily for you, I review speakers for a living, and this <a href="https://www.tomsguide.com/live/news/prime-day-2026-handpicked-editor-recommended-deals-live">Amazon Prime Day</a>, I've handpicked my top deals, starting from just $27.</p><p>Yep, the <a href="https://www.amazon.com/Tribit-Bluetooth-StormBox-Playtime-Waterproof/dp/B0DSPJZKPK/" target="_blank" rel="nofollow">Tribit StormBox Mini+ is down to just $27</a>! It's the speaker I use at home, and for its size, it can get very loud. But what about those who don't want a small speaker and want a big, boombox-style one with Earth-shaking bass? You're in luck: the <a href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow">JBL Xtreme 4 is 34% off right now</a>, and this deal saves you $130.</p><p>Fans of rock 'n' roll may want to consider picking up the <a href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow">Marshall Acton III, available for just $179 this Prime Day</a>, and its drool-worthy looks and tactile amp-like controls are a sight to behold. Or if you want a party speaker that doubles as a guitar amp <em>and </em>karaoke machine, how about the <a href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow">Marshall Bromley 750, which is down to its lowest-ever price of $999 right now</a>?</p><p>And there's a lot more where that came from! Keep reading for the best Prime Day speaker deals you can grab this week.</p><h2 id="meet-your-bluetooth-speakers-deals-expert">Meet your Bluetooth speakers deals expert</h2><h2 class="article-body__section" id="section-quick-links"><span>Quick links</span></h2><ul><li><strong></strong><a href="https://www.amazon.com/primeday" target="_blank" rel="nofollow"><strong>Shop all Amazon Prime Day deals now</strong></a><strong></strong></li><li><strong>JBL Flip 7: </strong><a href="https://www.amazon.com/JBL-Flip-Waterproof-Interchangeable-Accessories/dp/B0DMYQ32SC/" target="_blank" rel="nofollow"><strong>was $149 now $94</strong></a><strong></strong></li><li><strong>JBL Xtreme 4: </strong><a href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow"><strong>was $379 now $249</strong></a><strong></strong></li><li><strong>JBL Charge 6: </strong><a href="https://www.amazon.com/JBL-Charge-Waterproof-Drop-Proof-Built/dp/B0DN2ZCZX6/" target="_blank" rel="nofollow"><strong>was $199 now $159</strong></a><strong></strong></li><li><strong>JBL Go 5: </strong><a href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><strong>was $54 now $44</strong></a><strong></strong></li><li><strong>Bose SoundLink Max: </strong><a href="https://www.amazon.com/Bose-SoundLink-Portable-Waterproof-Bluetooth/dp/B0CVL1K7DX/" target="_blank" rel="nofollow"><strong>was $399 now $279</strong></a><strong></strong></li><li><strong>Bose SoundLink Flex (2nd Gen): </strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow"><strong>was $139 now $99</strong></a><strong></strong></li><li><strong>Marshall Emberton III: </strong><a href="https://www.amazon.com/Marshall-Emberton-Portable-Bluetooth-Speaker/dp/B0GXH3N61Z/" target="_blank" rel="nofollow"><strong>was $179 now $99</strong></a><strong></strong></li><li><strong>Marshall Acton III: </strong><a href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow"><strong>was $299 now $179</strong></a><strong></strong></li><li><strong>Marshall Stanmore III: </strong><a href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59/" target="_blank" rel="nofollow"><strong>was $399 now $249</strong></a><strong></strong></li><li><strong>Marshall Bromley 750: </strong><a href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow"><strong>was $1,299 now $999</strong></a><strong></strong></li><li><strong>Tribit StormBox Lava: </strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2/" target="_blank" rel="nofollow"><strong>was $139 now $94</strong></a><strong></strong></li><li><strong>Tribit StormBox Micro 3: </strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow"><strong>was $64 now $47</strong></a><strong></strong></li><li><strong>Tribit StormBox Mini+:</strong><a href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow"><strong> was $34 now $27</strong></a></li><li><strong>EarFun UBoom L: </strong><a href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow"><strong>was $79 now $52</strong></a></li><li><strong>8BitDo Retro Cube 2: </strong><a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow"><strong>was $55 now $49</strong></a></li></ul><h2 class="article-body__section" id="section-best-prime-day-jbl-deals"><span>Best Prime Day JBL deals</span></h2><div class="product"><a data-dimension112="a001cab0-5f1e-4a1f-8385-3005dec17a9d" data-action="Deal Block" data-label="Flip 7" data-dimension48="Flip 7" data-dimension25="$94" href="https://www.amazon.com/JBL-Flip-Waterproof-Interchangeable-Accessories/dp/B0DMYQ32SC/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6723px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="TSUFRZEoBKMVXB5HoEYo4d" name="JBL Flip 7" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/TSUFRZEoBKMVXB5HoEYo4d.jpg" mos="" align="middle" fullscreen="" width="6723" height="6723" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/i-just-tested-the-jbl-flip-7-is-it-the-best-compact-bluetooth-speaker-yet" data-dimension112="a001cab0-5f1e-4a1f-8385-3005dec17a9d" data-action="Deal Block" data-label="Flip 7" data-dimension48="Flip 7" data-dimension25="$94">JBL Flip 7</a> is small and powerful yet adamantly refuses to compromise on sound quality, particularly bass. There are very few minor things I’d change — let’s beef up that (admittedly still decent) battery life, enable stereo output on other JBL models, and crowbar in a low-latency mode for streaming and gaming.<a class="view-deal button" href="https://www.amazon.com/JBL-Flip-Waterproof-Interchangeable-Accessories/dp/B0DMYQ32SC/" target="_blank" rel="nofollow" data-dimension112="a001cab0-5f1e-4a1f-8385-3005dec17a9d" data-action="Deal Block" data-label="Flip 7" data-dimension48="Flip 7" data-dimension25="$94">View Deal</a></p></div><div class="product"><a data-dimension112="2b8c4704-662c-44dd-af51-d6466b70ee86" data-action="Deal Block" data-label="Xtreme 4" data-dimension48="Xtreme 4" data-dimension25="$249" href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:679px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="L2TjtWjKvMz35uXeyxCSKR" name="JBL Xtreme 4.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/L2TjtWjKvMz35uXeyxCSKR.jpg" mos="" align="middle" fullscreen="" width="679" height="679" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Because it has been succeeded by a 5th-gen speaker, now's a great time to buy the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-4-review" data-dimension112="2b8c4704-662c-44dd-af51-d6466b70ee86" data-action="Deal Block" data-label="Xtreme 4" data-dimension48="Xtreme 4" data-dimension25="$249">JBL Xtreme 4</a> as it has been discounted by $130. It boasts customizable EQ, PartyBoost with AuraCast, and ear-splitting (in a good way) volume. We love its IP67 waterproofing, easy-to-use app, and the fact that it doubles as a power bank.<a class="view-deal button" href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow" data-dimension112="2b8c4704-662c-44dd-af51-d6466b70ee86" data-action="Deal Block" data-label="Xtreme 4" data-dimension48="Xtreme 4" data-dimension25="$249">View Deal</a></p></div><div class="product"><a data-dimension112="3742ae23-0d1b-46f0-9f6f-edc0e572c44c" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="YHNWrEwxj5uzgYGXCTeiJ6" name="Go5-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/YHNWrEwxj5uzgYGXCTeiJ6.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review" data-dimension112="3742ae23-0d1b-46f0-9f6f-edc0e572c44c" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">JBL Go 5</a> boasts a wide soundstage packed into a compact, pocketable body. It looks great, sounds even better, and can be customized via a user-friendly companion app. It’s IP68-rated, so it’s highly waterproof, dustproof and drop-proof too.<a class="view-deal button" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow" data-dimension112="3742ae23-0d1b-46f0-9f6f-edc0e572c44c" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-bose-deals"><span>Best Prime Day Bose deals</span></h2><div class="product"><a data-dimension112="cf2b00d3-f036-45fa-aa66-5596966fae63" data-action="Deal Block" data-label="SoundLink Max" data-dimension48="SoundLink Max" data-dimension25="$279" href="https://www.amazon.com/Bose-SoundLink-Portable-Waterproof-Bluetooth/dp/B0CVL1K7DX/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:958px;"><p class="vanilla-image-block" style="padding-top:83.72%;"><img id="xFfX4WEKpF3opzDYZMKnhB" name="Bose SoundLink Max.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/xFfX4WEKpF3opzDYZMKnhB.jpg" mos="" align="middle" fullscreen="" width="958" height="802" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A great addition to Bose's Bluetooth speaker lineup, the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/bose-soundlink-max" data-dimension112="cf2b00d3-f036-45fa-aa66-5596966fae63" data-action="Deal Block" data-label="SoundLink Max" data-dimension48="SoundLink Max" data-dimension25="$279">SoundLink Max</a> delivers expansive stereo sound, a 20-hour battery life and is one of the most practical portable speakers for life on the move. It boasts a wide soundstage and can get very loud without music sounding distorted.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Portable-Waterproof-Bluetooth/dp/B0CVL1K7DX/" target="_blank" rel="nofollow" data-dimension112="cf2b00d3-f036-45fa-aa66-5596966fae63" data-action="Deal Block" data-label="SoundLink Max" data-dimension48="SoundLink Max" data-dimension25="$279">View Deal</a></p></div><div class="product"><a data-dimension112="307842e7-8741-428a-806a-1b09eb8511a2" data-action="Deal Block" data-label="SoundLink Flex (2nd Gen)" data-dimension48="SoundLink Flex (2nd Gen)" data-dimension25="$99" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1303px;"><p class="vanilla-image-block" style="padding-top:74.75%;"><img id="hfwnfPdrGwgnEcbUXqjbCP" name="SoundLink Flex Bluetooth Speaker (2nd gen)" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/hfwnfPdrGwgnEcbUXqjbCP.png" mos="" align="middle" fullscreen="" width="1303" height="974" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow" data-dimension112="307842e7-8741-428a-806a-1b09eb8511a2" data-action="Deal Block" data-label="SoundLink Flex (2nd Gen)" data-dimension48="SoundLink Flex (2nd Gen)" data-dimension25="$99">SoundLink Flex (2nd Gen): was $139 now $99</a></strong><br>It’s slightly more expensive than the competition, but the <a href="https://www.tomsguide.com/audio/bose-soundlink-flex-2-review">Bose SoundLink Flex Gen 2</a> justifies its higher price with great sound quality and app connectivity. I think it looks great, too, and the new color options make for a super fashionable portable speaker.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow" data-dimension112="307842e7-8741-428a-806a-1b09eb8511a2" data-action="Deal Block" data-label="SoundLink Flex (2nd Gen)" data-dimension48="SoundLink Flex (2nd Gen)" data-dimension25="$99">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-marshall-deals"><span>Best Prime Day Marshall deals</span></h2><div class="product"><a data-dimension112="cd64f3a0-6682-493c-b30b-cc95d48ffc00" data-action="Deal Block" data-label="Emberton III" data-dimension48="Emberton III" data-dimension25="$99" href="https://www.amazon.com/Marshall-Emberton-Portable-Bluetooth-Speaker/dp/B0GXH3N61Z/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1509px;"><p class="vanilla-image-block" style="padding-top:99.34%;"><img id="HTvaeX4MhcAV7Mw3cX4oUM" name="marshall Emberton III.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/HTvaeX4MhcAV7Mw3cX4oUM.jpg" mos="" align="middle" fullscreen="" width="1509" height="1499" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/marshal-emberton-iii-review" data-dimension112="cd64f3a0-6682-493c-b30b-cc95d48ffc00" data-action="Deal Block" data-label="Emberton III" data-dimension48="Emberton III" data-dimension25="$99">Marshall Emberton III</a> is, in one word, epic. It is one of the brand's smaller speakers, but it packs solid sound into a very tasty-looking design. From the control knob found on loads of Marshall gear to the impactful sound, the Emberton III is a great Bluetooth speaker if you’re looking for something unique and stylish.<a class="view-deal button" href="https://www.amazon.com/Marshall-Emberton-Portable-Bluetooth-Speaker/dp/B0GXH3N61Z/" target="_blank" rel="nofollow" data-dimension112="cd64f3a0-6682-493c-b30b-cc95d48ffc00" data-action="Deal Block" data-label="Emberton III" data-dimension48="Emberton III" data-dimension25="$99">View Deal</a></p></div><div class="product"><a data-dimension112="a5fbf596-9366-4937-a1e8-b62788fe2dd9" data-action="Deal Block" data-label="Acton III" data-dimension48="Acton III" data-dimension25="$179" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="T2qSjTer2AgBjuQkKpsHm" name="Marshall Acton III" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/T2qSjTer2AgBjuQkKpsHm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Acton III is a retro-looking Bluetooth speaker that can actually make your setup feel more modern. The physical controls are tactile and satisfying, making sure it's easy for you to get the sound you want. Thanks to its large cabinet size and quality audio engineering inside, it's hard to be disappointed in the Acton III. The bass is also super responsive, making it a top-tier choice for parties.<a class="view-deal button" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow" data-dimension112="a5fbf596-9366-4937-a1e8-b62788fe2dd9" data-action="Deal Block" data-label="Acton III" data-dimension48="Acton III" data-dimension25="$179">View Deal</a></p></div><div class="product"><a data-dimension112="accafa83-ab63-4dfe-9a8f-8b932991e988" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$249" href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="wYnAg4Aq77FqRs4Sxa3xn7" name="marshall stanmore iii" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wYnAg4Aq77FqRs4Sxa3xn7.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Stanmore III completes Marshall's Homeline III lineup, and it's perfect for small spaces as it measures just 13.78 x 8 x 7.4 inches. It packs Class D amplification rated at 50W for the single mid/woofer, while two 15W amps take care of the tweeters. It sounds incredible, and you should get it just for the rock 'n' roll design, in my opinion.<a class="view-deal button" href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59/" target="_blank" rel="nofollow" data-dimension112="accafa83-ab63-4dfe-9a8f-8b932991e988" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$249">View Deal</a></p></div><div class="product"><a data-dimension112="1ecb1636-8192-430e-ac69-55c413def5da" data-action="Deal Block" data-label="Bromley 750" data-dimension48="Bromley 750" data-dimension25="$999" href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="hC3fb877MKHEkgSJ57m2nm" name="Bromley-750-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/hC3fb877MKHEkgSJ57m2nm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Thanks to a plethora of wired and wireless connectivity options, the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review" data-dimension112="1ecb1636-8192-430e-ac69-55c413def5da" data-action="Deal Block" data-label="Bromley 750" data-dimension48="Bromley 750" data-dimension25="$999">Marshall Bromley 750</a> party speaker doubles (or triples) as a guitar amp and a karaoke machine, while delivering powerful sound quality when you’re listening to music. A massive 500W power output makes the bass sound loud and refined, while vocals are crystal clear.<a class="view-deal button" href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow" data-dimension112="1ecb1636-8192-430e-ac69-55c413def5da" data-action="Deal Block" data-label="Bromley 750" data-dimension48="Bromley 750" data-dimension25="$999">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-tribit-deals"><span>Best Prime Day Tribit deals</span></h2><div class="product"><a data-dimension112="c4f5f2fb-76f4-4803-8eb6-ee6b7125a3e3" data-action="Deal Block" data-label="StormBox Lava" data-dimension48="StormBox Lava" data-dimension25="$94" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:679px;"><p class="vanilla-image-block" style="padding-top:83.80%;"><img id="GNykPgtqnDpV9wfnHuMJ9L" name="Tribit StormBox Lava Bluetooth Speaker in black" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GNykPgtqnDpV9wfnHuMJ9L.jpg" mos="" align="middle" fullscreen="" width="679" height="569" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review" data-dimension112="c4f5f2fb-76f4-4803-8eb6-ee6b7125a3e3" data-action="Deal Block" data-label="StormBox Lava" data-dimension48="StormBox Lava" data-dimension25="$94">Tribit StormBox Lava</a> packs a punch but won’t break the bank. The sleek design means it’s easily portable, while water- and dustproofing equip it for any situation. The bass will shake the ground around you, but it still manages to produce a well-rounded sound with crisp treble.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2/" target="_blank" rel="nofollow" data-dimension112="c4f5f2fb-76f4-4803-8eb6-ee6b7125a3e3" data-action="Deal Block" data-label="StormBox Lava" data-dimension48="StormBox Lava" data-dimension25="$94">View Deal</a></p></div><div class="product"><a data-dimension112="68b15a7e-50f3-407f-939a-610865db8544" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$47" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1110px;"><p class="vanilla-image-block" style="padding-top:118.65%;"><img id="dsAxZA3JpSDwfF643VP3eB" name="Micro 3 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/dsAxZA3JpSDwfF643VP3eB.jpg" mos="" align="middle" fullscreen="" width="1110" height="1317" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review" data-dimension112="68b15a7e-50f3-407f-939a-610865db8544" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$47">Tribit StormBox Micro 3 </a>offers an IP68 rating, a companion app for EQ customization (although some settings cause compression), and simple controls that make the speaker a breeze to use. It has 24 hours of battery life and can even be used as a portable charger. But most importantly, the audio performance really is excellent.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow" data-dimension112="68b15a7e-50f3-407f-939a-610865db8544" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$47">View Deal</a></p></div><div class="product"><a data-dimension112="d1e57288-124a-4492-a264-515519343437" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$27" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1480px;"><p class="vanilla-image-block" style="padding-top:101.35%;"><img id="iRvkqAH3YBcTTf4zRa7AdC" name="Tribit-StormBox-Mini-Plus-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/iRvkqAH3YBcTTf4zRa7AdC.jpg" mos="" align="middle" fullscreen="" width="1480" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review" data-dimension112="d1e57288-124a-4492-a264-515519343437" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$27">Tribit StormBox Mini+</a> boasts powerful bass response and detailed vocals. It also sports reactive LED lighting that’s fun to watch, and IPX7 waterproofing so it can be used poolside or in the shower. For the money, I recommend buying two so you can take advantage of the dreamy stereo sound.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow" data-dimension112="d1e57288-124a-4492-a264-515519343437" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$27">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-miscellaneous-deals"><span>Best Prime Day miscellaneous deals</span></h2><div class="product"><a data-dimension112="130232fe-27d8-4ce2-98e7-3ce3c65377d1" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$52" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1115px;"><p class="vanilla-image-block" style="padding-top:49.42%;"><img id="U4eB79RfdkjMCrjY6xNuFm" name="EarFun-UBoomL-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/U4eB79RfdkjMCrjY6xNuFm.png" mos="" align="middle" fullscreen="" width="1115" height="551" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/speakers/bluetooth-speakers/earfun-uboom-l-review" data-dimension112="130232fe-27d8-4ce2-98e7-3ce3c65377d1" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$52">EarFun UBoom L</a> offers big sound on a budget. This portable speaker produces powerful bass, clear vocals and well-balanced sound across a range of genres. With a dedicated outdoor mode and an IP67 resistance rating, the speaker can soundtrack pool parties and hikes. It has great battery life, and the customizable EQ via the user-friendly companion app works really well.<a class="view-deal button" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow" data-dimension112="130232fe-27d8-4ce2-98e7-3ce3c65377d1" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$52">View Deal</a></p></div><div class="product"><a data-dimension112="549ce9e6-839f-4f0e-bda8-e1af2c057c61" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:892px;"><p class="vanilla-image-block" style="padding-top:116.82%;"><img id="r5zUUMfK2PzaubVDbQfHCF" name="8BitDo-RetroCube2-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/r5zUUMfK2PzaubVDbQfHCF.jpg" mos="" align="middle" fullscreen="" width="892" height="1042" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review" data-dimension112="549ce9e6-839f-4f0e-bda8-e1af2c057c61" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49">8BitDo Retro Cube 2</a> is the quirkiest speaker around, and that’s down to its NES-inspired design and D-pad-style controls. The speaker comes with a wireless charging dock with a slot to store the 2.4GHz dongle safely, and it boasts class-leading battery life of 30 hours. Crisp treble and a refined midrange make the speaker shine.<a class="view-deal button" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow" data-dimension112="549ce9e6-839f-4f0e-bda8-e1af2c057c61" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49">View Deal</a></p></div><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-speaker-deals"><span>We're tracking all the best Prime Day speaker deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Speakers&min_discount_ratio=0.95&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }        .tg-df-container .tg-df-carousel-scroll-left,    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-left { left: 8px; }    .tg-df-container .tg-df-carousel-scroll-right { right: 8px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left { left: 0px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { right: 0px; }    .tg-df-carousel-filters-outer { margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; }    @container tg-df (max-width: 599px) { .tg-df-carousel-filters-outer { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; } }        .tg-df-container .tg-df-carousel-scroll-left:hover,    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right {      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right { right: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-right { right: 0; }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left:hover,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-outer" style="position: relative;">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>        \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3C/div>    \x3C/div>    \x3C/div>      \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>      \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }          const website = params.get('website') || 'tomsguide';          this.website = website;          if (website === 'techradar') {            const squadHeader = this.root.querySelector('.tg-df-savings-squad-header');            if (squadHeader) {               const pic = squadHeader.querySelector('picture');               if (pic) pic.style.display = 'none';            }            const style = document.createElement('style');            style.innerHTML = `              .tg-df-container .hawk-affiliate-link-deal-button { background-color: #5DAF08 !important; }              .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a8c06 !important; }            `;            this.root.appendChild(style);          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }          this.setupScrollListeners();        }        setupScrollListeners() {          const containers = [             this.root.querySelector('.tg-df-carousel-roundels'),             this.root.querySelector('.tg-df-carousel-filters-wrap'),             this.root.querySelector('#tg-df-grid')          ];                    containers.forEach(container => {             if (!container) return;                          const checkScroll = () => {                if (!container.parentElement) return;                const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left');                const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right');                                if (leftBtn) {                   if (container.scrollLeft <= 5) leftBtn.style.display = 'none';                   else leftBtn.style.display = 'flex';                }                                if (rightBtn) {                   if (container.scrollWidth <= container.clientWidth) {                       rightBtn.style.display = 'none';                   } else if (container.scrollLeft >= container.scrollWidth - container.clientWidth - 5) {                       rightBtn.style.display = 'none';                   } else {                       rightBtn.style.display = 'flex';                   }                }             };                          container.addEventListener('scroll', checkScroll);             checkScroll();                          window.addEventListener('resize', checkScroll);                          const observer = new MutationObserver(checkScroll);             observer.observe(container, { childList: true, subtree: true, characterData: false });          });        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          } else {             this.displayLimit += (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');             const encodedTag = encodeURIComponent(cleanTag);             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    if (this.rowsSelect && this.rowsSelect.value) {            url.searchParams.append('rows', this.rowsSelect.value);          } else {             url.searchParams.append('rows', '12'); // default          }          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = 12;          if (this.rowsSelect && this.rowsSelect.value) {            rowLimit = parseInt(this.rowsSelect.value, 10) || 12;          }          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map((v, idx) => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              const revenueIdVal = generateRevenueId(linkUrl, offerName, merchantName, null);              const rewrittenLinkUrl = rewriteAffiliateLink(linkUrl, area, revenueIdVal);              return `                \x3Ca href="${this.escapeHTML(rewrittenLinkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item"                  data-action="voucher-click"                  data-product-name="${this.escapeHTML(offerName)}"                  data-merchant-name="${this.escapeHTML(merchantName)}"                  data-analytics-id="${this.escapeHTML(offerObj.offer_id || offerObj.id || v.id || '')}"                  data-price=""                  data-previous-price=""                  data-original-link="${this.escapeHTML(linkUrl)}"                  data-revenue-id="${revenueIdVal}"                  data-index="${idx}"                  data-total="${offers.length}"                  data-in-stock="true"                  data-currency="USD"                  data-model-id="${this.escapeHTML(offerObj.model_id || v.model_id || offerObj.id || v.id || '')}"                  data-merchant-id="${this.escapeHTML(offerObj.merchant_id || offerObj.merchant?.id || '')}"                >                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });            // Attach voucher click tracking            const voucherBtns = content.querySelectorAll('[data-action="voucher-click"]');            voucherBtns.forEach(btn => {              btn.addEventListener('click', (e) => {                if (e.target.closest('[data-action="copy-code"]')) return;                                const productName = btn.getAttribute('data-product-name');                const merchantNameAttr = btn.getAttribute('data-merchant-name');                const productId = btn.getAttribute('data-analytics-id');                const price = parseFloat(btn.getAttribute('data-price')) || null;                const prevPriceStr = btn.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = btn.getAttribute('data-original-link');                const rewrittenLink = btn.getAttribute('href');                const revenueId = btn.getAttribute('data-revenue-id');                const index = parseInt(btn.getAttribute('data-index'), 10) || 0;                const inStock = btn.getAttribute('data-in-stock') === 'true';                const totalText = btn.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: btn.getAttribute('data-model-id') || null,                    matchId: btn.getAttribute('data-match-id') || null,                    brand: btn.getAttribute('data-model-brand') || null,                    parent: btn.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: btn.getAttribute('data-merchant-id') || null,                    network: btn.getAttribute('data-merchant-network') || null,                    url: btn.getAttribute('data-merchant-url') || null,                    name: merchantNameAttr                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: btn.getAttribute('data-currency') || 'USD'                };                if (typeof trackDealClick === 'function') {                  trackDealClick(trackingParams);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;          // Inject JSON-LD          try {            let targetNode = this.hostContainer || document.head;            let jsonLdScript = targetNode.querySelector('#tg-df-json-ld-' + this.widgetId);            if (!jsonLdScript) {                jsonLdScript = document.createElement('script');                jsonLdScript.type = 'application/ld+json';                jsonLdScript.id = 'tg-df-json-ld-' + this.widgetId;                targetNode.appendChild(jsonLdScript);            }            const jsonLdData = {              "@context": "https://schema.org",              "@type": "ItemList",              "itemListElement": displayDeals.slice(0, this.displayLimit).map((deal, index) => {                 let isoCurrency = "USD";                 if (deal.currency === '£') isoCurrency = "GBP";                 else if (deal.currency === '€') isoCurrency = "EUR";                 else if (deal.currency === 'A$') isoCurrency = "AUD";                 else if (deal.currency === 'CA$') isoCurrency = "CAD";                 const areaCode = typeof this.getAreaCode === 'function' ? this.getAreaCode() : 'US';                 const revenueId = typeof generateRevenueId === 'function' ? generateRevenueId(deal.url, deal.title, deal.merchant, null) : '';                 const rewrittenLink = typeof rewriteAffiliateLink === 'function' ? rewriteAffiliateLink(deal.url, areaCode, revenueId) : deal.url;                 return {                   "@type": "ListItem",                   "position": index + 1,                   "item": {                     "@type": "Product",                     "name": deal.title,                     "image": deal.image || "",                     "description": deal.description || "",                     "brand": {                       "@type": "Brand",                       "name": deal.brand || ""                     },                     "offers": {                       "@type": "Offer",                       "priceCurrency": isoCurrency,                       "price": deal.rawPrice || 0,                       "url": rewrittenLink,                       "seller": {                         "@type": "Organization",                         "name": deal.merchant || ""                       }                     }                   }                 };              }).filter(item => item.item.name)            };            jsonLdScript.textContent = JSON.stringify(jsonLdData);          } catch(e) { console.warn("JSON-LD generation failed", e); }                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let rightChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             let leftChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-left');             if (this.getViewMode() === 'carousel') {                 // The observer set up in setupScrollListeners handles visibility.                 if (rightChevron) rightChevron.style.display = 'flex';                 if (leftChevron) leftChevron.style.display = 'none'; // reset correctly             } else {                 if (rightChevron) rightChevron.style.display = 'none';                 if (leftChevron) leftChevron.style.display = 'none';             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12);                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            if (!this.processArticleFired) {                  this.processArticleFired = true;                  document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));               }            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/amazon-prime-day-2026-bluetooth-speakers-deals</link>
                                                                            <description>
                            <![CDATA[ Whether you want to soundtrack a party, do karaoke in the shower, or simply listen to music out loud, I've found the best Prime Day deals on Bose, JBL and more. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">JuJkjagRgfDDULXKWPUuKM</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/TwtG5aUn6cyrzZVgnMrPTe-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 23 Jun 2026 18:12:37 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/TwtG5aUn6cyrzZVgnMrPTe-1280-80.jpg">
                                                            <media:credit><![CDATA[Marshall / Bose / JBL / Tribit / Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A selection of Bluetooth speakers]]></media:description>                                                            <media:text><![CDATA[A selection of Bluetooth speakers]]></media:text>
                                <media:title type="plain"><![CDATA[A selection of Bluetooth speakers]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/TwtG5aUn6cyrzZVgnMrPTe-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I don't know about you, but I love listening to music whenever possible. Though I usually use earbuds or headphones to do so, I like jamming to my favorite tunes by listening to them through the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> too, especially if I'm hanging out with my partner. Or soundtracking a party. Or performing karaoke in the shower. Or dancing to Tame Impala while I cook.</p><p>Technology has come leaps and bounds, and today, we're spoilt for choice when it comes to speakers. With so many choices, where do you even start? Luckily for you, I review speakers for a living, and this <a href="https://www.tomsguide.com/live/news/prime-day-2026-handpicked-editor-recommended-deals-live">Amazon Prime Day</a>, I've handpicked my top deals, starting from just $27.</p><p>Yep, the <a href="https://www.amazon.com/Tribit-Bluetooth-StormBox-Playtime-Waterproof/dp/B0DSPJZKPK/" target="_blank" rel="nofollow">Tribit StormBox Mini+ is down to just $27</a>! It's the speaker I use at home, and for its size, it can get very loud. But what about those who don't want a small speaker and want a big, boombox-style one with Earth-shaking bass? You're in luck: the <a href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow">JBL Xtreme 4 is 34% off right now</a>, and this deal saves you $130.</p><p>Fans of rock 'n' roll may want to consider picking up the <a href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow">Marshall Acton III, available for just $179 this Prime Day</a>, and its drool-worthy looks and tactile amp-like controls are a sight to behold. Or if you want a party speaker that doubles as a guitar amp <em>and </em>karaoke machine, how about the <a href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow">Marshall Bromley 750, which is down to its lowest-ever price of $999 right now</a>?</p><p>And there's a lot more where that came from! Keep reading for the best Prime Day speaker deals you can grab this week.</p><h2 id="meet-your-bluetooth-speakers-deals-expert">Meet your Bluetooth speakers deals expert</h2><h2 class="article-body__section" id="section-quick-links"><span>Quick links</span></h2><ul><li><strong></strong><a href="https://www.amazon.com/primeday" target="_blank" rel="nofollow"><strong>Shop all Amazon Prime Day deals now</strong></a><strong></strong></li><li><strong>JBL Flip 7: </strong><a href="https://www.amazon.com/JBL-Flip-Waterproof-Interchangeable-Accessories/dp/B0DMYQ32SC/" target="_blank" rel="nofollow"><strong>was $149 now $94</strong></a><strong></strong></li><li><strong>JBL Xtreme 4: </strong><a href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow"><strong>was $379 now $249</strong></a><strong></strong></li><li><strong>JBL Charge 6: </strong><a href="https://www.amazon.com/JBL-Charge-Waterproof-Drop-Proof-Built/dp/B0DN2ZCZX6/" target="_blank" rel="nofollow"><strong>was $199 now $159</strong></a><strong></strong></li><li><strong>JBL Go 5: </strong><a href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><strong>was $54 now $44</strong></a><strong></strong></li><li><strong>Bose SoundLink Max: </strong><a href="https://www.amazon.com/Bose-SoundLink-Portable-Waterproof-Bluetooth/dp/B0CVL1K7DX/" target="_blank" rel="nofollow"><strong>was $399 now $279</strong></a><strong></strong></li><li><strong>Bose SoundLink Flex (2nd Gen): </strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow"><strong>was $139 now $99</strong></a><strong></strong></li><li><strong>Marshall Emberton III: </strong><a href="https://www.amazon.com/Marshall-Emberton-Portable-Bluetooth-Speaker/dp/B0GXH3N61Z/" target="_blank" rel="nofollow"><strong>was $179 now $99</strong></a><strong></strong></li><li><strong>Marshall Acton III: </strong><a href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow"><strong>was $299 now $179</strong></a><strong></strong></li><li><strong>Marshall Stanmore III: </strong><a href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59/" target="_blank" rel="nofollow"><strong>was $399 now $249</strong></a><strong></strong></li><li><strong>Marshall Bromley 750: </strong><a href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow"><strong>was $1,299 now $999</strong></a><strong></strong></li><li><strong>Tribit StormBox Lava: </strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2/" target="_blank" rel="nofollow"><strong>was $139 now $94</strong></a><strong></strong></li><li><strong>Tribit StormBox Micro 3: </strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow"><strong>was $64 now $47</strong></a><strong></strong></li><li><strong>Tribit StormBox Mini+:</strong><a href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow"><strong> was $34 now $27</strong></a></li><li><strong>EarFun UBoom L: </strong><a href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow"><strong>was $79 now $52</strong></a></li><li><strong>8BitDo Retro Cube 2: </strong><a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow"><strong>was $55 now $49</strong></a></li></ul><h2 class="article-body__section" id="section-best-prime-day-jbl-deals"><span>Best Prime Day JBL deals</span></h2><div class="product"><a data-dimension112="a001cab0-5f1e-4a1f-8385-3005dec17a9d" data-action="Deal Block" data-label="Flip 7" data-dimension48="Flip 7" data-dimension25="$94" href="https://www.amazon.com/JBL-Flip-Waterproof-Interchangeable-Accessories/dp/B0DMYQ32SC/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6723px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="TSUFRZEoBKMVXB5HoEYo4d" name="JBL Flip 7" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/TSUFRZEoBKMVXB5HoEYo4d.jpg" mos="" align="middle" fullscreen="" width="6723" height="6723" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/i-just-tested-the-jbl-flip-7-is-it-the-best-compact-bluetooth-speaker-yet" data-dimension112="a001cab0-5f1e-4a1f-8385-3005dec17a9d" data-action="Deal Block" data-label="Flip 7" data-dimension48="Flip 7" data-dimension25="$94">JBL Flip 7</a> is small and powerful yet adamantly refuses to compromise on sound quality, particularly bass. There are very few minor things I’d change — let’s beef up that (admittedly still decent) battery life, enable stereo output on other JBL models, and crowbar in a low-latency mode for streaming and gaming.<a class="view-deal button" href="https://www.amazon.com/JBL-Flip-Waterproof-Interchangeable-Accessories/dp/B0DMYQ32SC/" target="_blank" rel="nofollow" data-dimension112="a001cab0-5f1e-4a1f-8385-3005dec17a9d" data-action="Deal Block" data-label="Flip 7" data-dimension48="Flip 7" data-dimension25="$94">View Deal</a></p></div><div class="product"><a data-dimension112="2b8c4704-662c-44dd-af51-d6466b70ee86" data-action="Deal Block" data-label="Xtreme 4" data-dimension48="Xtreme 4" data-dimension25="$249" href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:679px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="L2TjtWjKvMz35uXeyxCSKR" name="JBL Xtreme 4.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/L2TjtWjKvMz35uXeyxCSKR.jpg" mos="" align="middle" fullscreen="" width="679" height="679" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Because it has been succeeded by a 5th-gen speaker, now's a great time to buy the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-4-review" data-dimension112="2b8c4704-662c-44dd-af51-d6466b70ee86" data-action="Deal Block" data-label="Xtreme 4" data-dimension48="Xtreme 4" data-dimension25="$249">JBL Xtreme 4</a> as it has been discounted by $130. It boasts customizable EQ, PartyBoost with AuraCast, and ear-splitting (in a good way) volume. We love its IP67 waterproofing, easy-to-use app, and the fact that it doubles as a power bank.<a class="view-deal button" href="https://www.amazon.com/JBL-Xtreme-Waterproof-PartyBoost-Multi-speaker/dp/B0CTP191Z3/" target="_blank" rel="nofollow" data-dimension112="2b8c4704-662c-44dd-af51-d6466b70ee86" data-action="Deal Block" data-label="Xtreme 4" data-dimension48="Xtreme 4" data-dimension25="$249">View Deal</a></p></div><div class="product"><a data-dimension112="3742ae23-0d1b-46f0-9f6f-edc0e572c44c" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="YHNWrEwxj5uzgYGXCTeiJ6" name="Go5-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/YHNWrEwxj5uzgYGXCTeiJ6.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review" data-dimension112="3742ae23-0d1b-46f0-9f6f-edc0e572c44c" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">JBL Go 5</a> boasts a wide soundstage packed into a compact, pocketable body. It looks great, sounds even better, and can be customized via a user-friendly companion app. It’s IP68-rated, so it’s highly waterproof, dustproof and drop-proof too.<a class="view-deal button" href="https://www.amazon.com/JBL-Ultra-Portable-Waterproof-Drop-Proof-Compatible/dp/B0GHY1H2JS/" target="_blank" rel="nofollow" data-dimension112="3742ae23-0d1b-46f0-9f6f-edc0e572c44c" data-action="Deal Block" data-label="Go 5" data-dimension48="Go 5" data-dimension25="$44">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-bose-deals"><span>Best Prime Day Bose deals</span></h2><div class="product"><a data-dimension112="cf2b00d3-f036-45fa-aa66-5596966fae63" data-action="Deal Block" data-label="SoundLink Max" data-dimension48="SoundLink Max" data-dimension25="$279" href="https://www.amazon.com/Bose-SoundLink-Portable-Waterproof-Bluetooth/dp/B0CVL1K7DX/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:958px;"><p class="vanilla-image-block" style="padding-top:83.72%;"><img id="xFfX4WEKpF3opzDYZMKnhB" name="Bose SoundLink Max.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/xFfX4WEKpF3opzDYZMKnhB.jpg" mos="" align="middle" fullscreen="" width="958" height="802" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A great addition to Bose's Bluetooth speaker lineup, the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/bose-soundlink-max" data-dimension112="cf2b00d3-f036-45fa-aa66-5596966fae63" data-action="Deal Block" data-label="SoundLink Max" data-dimension48="SoundLink Max" data-dimension25="$279">SoundLink Max</a> delivers expansive stereo sound, a 20-hour battery life and is one of the most practical portable speakers for life on the move. It boasts a wide soundstage and can get very loud without music sounding distorted.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Portable-Waterproof-Bluetooth/dp/B0CVL1K7DX/" target="_blank" rel="nofollow" data-dimension112="cf2b00d3-f036-45fa-aa66-5596966fae63" data-action="Deal Block" data-label="SoundLink Max" data-dimension48="SoundLink Max" data-dimension25="$279">View Deal</a></p></div><div class="product"><a data-dimension112="307842e7-8741-428a-806a-1b09eb8511a2" data-action="Deal Block" data-label="SoundLink Flex (2nd Gen)" data-dimension48="SoundLink Flex (2nd Gen)" data-dimension25="$99" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1303px;"><p class="vanilla-image-block" style="padding-top:74.75%;"><img id="hfwnfPdrGwgnEcbUXqjbCP" name="SoundLink Flex Bluetooth Speaker (2nd gen)" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/hfwnfPdrGwgnEcbUXqjbCP.png" mos="" align="middle" fullscreen="" width="1303" height="974" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow" data-dimension112="307842e7-8741-428a-806a-1b09eb8511a2" data-action="Deal Block" data-label="SoundLink Flex (2nd Gen)" data-dimension48="SoundLink Flex (2nd Gen)" data-dimension25="$99">SoundLink Flex (2nd Gen): was $139 now $99</a></strong><br>It’s slightly more expensive than the competition, but the <a href="https://www.tomsguide.com/audio/bose-soundlink-flex-2-review">Bose SoundLink Flex Gen 2</a> justifies its higher price with great sound quality and app connectivity. I think it looks great, too, and the new color options make for a super fashionable portable speaker.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0DV5H3S16/" target="_blank" rel="nofollow" data-dimension112="307842e7-8741-428a-806a-1b09eb8511a2" data-action="Deal Block" data-label="SoundLink Flex (2nd Gen)" data-dimension48="SoundLink Flex (2nd Gen)" data-dimension25="$99">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-marshall-deals"><span>Best Prime Day Marshall deals</span></h2><div class="product"><a data-dimension112="cd64f3a0-6682-493c-b30b-cc95d48ffc00" data-action="Deal Block" data-label="Emberton III" data-dimension48="Emberton III" data-dimension25="$99" href="https://www.amazon.com/Marshall-Emberton-Portable-Bluetooth-Speaker/dp/B0GXH3N61Z/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1509px;"><p class="vanilla-image-block" style="padding-top:99.34%;"><img id="HTvaeX4MhcAV7Mw3cX4oUM" name="marshall Emberton III.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/HTvaeX4MhcAV7Mw3cX4oUM.jpg" mos="" align="middle" fullscreen="" width="1509" height="1499" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/marshal-emberton-iii-review" data-dimension112="cd64f3a0-6682-493c-b30b-cc95d48ffc00" data-action="Deal Block" data-label="Emberton III" data-dimension48="Emberton III" data-dimension25="$99">Marshall Emberton III</a> is, in one word, epic. It is one of the brand's smaller speakers, but it packs solid sound into a very tasty-looking design. From the control knob found on loads of Marshall gear to the impactful sound, the Emberton III is a great Bluetooth speaker if you’re looking for something unique and stylish.<a class="view-deal button" href="https://www.amazon.com/Marshall-Emberton-Portable-Bluetooth-Speaker/dp/B0GXH3N61Z/" target="_blank" rel="nofollow" data-dimension112="cd64f3a0-6682-493c-b30b-cc95d48ffc00" data-action="Deal Block" data-label="Emberton III" data-dimension48="Emberton III" data-dimension25="$99">View Deal</a></p></div><div class="product"><a data-dimension112="a5fbf596-9366-4937-a1e8-b62788fe2dd9" data-action="Deal Block" data-label="Acton III" data-dimension48="Acton III" data-dimension25="$179" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="T2qSjTer2AgBjuQkKpsHm" name="Marshall Acton III" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/T2qSjTer2AgBjuQkKpsHm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Acton III is a retro-looking Bluetooth speaker that can actually make your setup feel more modern. The physical controls are tactile and satisfying, making sure it's easy for you to get the sound you want. Thanks to its large cabinet size and quality audio engineering inside, it's hard to be disappointed in the Acton III. The bass is also super responsive, making it a top-tier choice for parties.<a class="view-deal button" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Black/dp/B0BC27MM5Z/" target="_blank" rel="nofollow" data-dimension112="a5fbf596-9366-4937-a1e8-b62788fe2dd9" data-action="Deal Block" data-label="Acton III" data-dimension48="Acton III" data-dimension25="$179">View Deal</a></p></div><div class="product"><a data-dimension112="accafa83-ab63-4dfe-9a8f-8b932991e988" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$249" href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="wYnAg4Aq77FqRs4Sxa3xn7" name="marshall stanmore iii" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wYnAg4Aq77FqRs4Sxa3xn7.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Stanmore III completes Marshall's Homeline III lineup, and it's perfect for small spaces as it measures just 13.78 x 8 x 7.4 inches. It packs Class D amplification rated at 50W for the single mid/woofer, while two 15W amps take care of the tweeters. It sounds incredible, and you should get it just for the rock 'n' roll design, in my opinion.<a class="view-deal button" href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59/" target="_blank" rel="nofollow" data-dimension112="accafa83-ab63-4dfe-9a8f-8b932991e988" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$249">View Deal</a></p></div><div class="product"><a data-dimension112="1ecb1636-8192-430e-ac69-55c413def5da" data-action="Deal Block" data-label="Bromley 750" data-dimension48="Bromley 750" data-dimension25="$999" href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="hC3fb877MKHEkgSJ57m2nm" name="Bromley-750-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/hC3fb877MKHEkgSJ57m2nm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Thanks to a plethora of wired and wireless connectivity options, the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review" data-dimension112="1ecb1636-8192-430e-ac69-55c413def5da" data-action="Deal Block" data-label="Bromley 750" data-dimension48="Bromley 750" data-dimension25="$999">Marshall Bromley 750</a> party speaker doubles (or triples) as a guitar amp and a karaoke machine, while delivering powerful sound quality when you’re listening to music. A massive 500W power output makes the bass sound loud and refined, while vocals are crystal clear.<a class="view-deal button" href="https://www.amazon.com/Marshall-Bromley-Portable-Wireless-Speaker/dp/B0FTZXFVJ1/" target="_blank" rel="nofollow" data-dimension112="1ecb1636-8192-430e-ac69-55c413def5da" data-action="Deal Block" data-label="Bromley 750" data-dimension48="Bromley 750" data-dimension25="$999">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-tribit-deals"><span>Best Prime Day Tribit deals</span></h2><div class="product"><a data-dimension112="c4f5f2fb-76f4-4803-8eb6-ee6b7125a3e3" data-action="Deal Block" data-label="StormBox Lava" data-dimension48="StormBox Lava" data-dimension25="$94" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:679px;"><p class="vanilla-image-block" style="padding-top:83.80%;"><img id="GNykPgtqnDpV9wfnHuMJ9L" name="Tribit StormBox Lava Bluetooth Speaker in black" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GNykPgtqnDpV9wfnHuMJ9L.jpg" mos="" align="middle" fullscreen="" width="679" height="569" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review" data-dimension112="c4f5f2fb-76f4-4803-8eb6-ee6b7125a3e3" data-action="Deal Block" data-label="StormBox Lava" data-dimension48="StormBox Lava" data-dimension25="$94">Tribit StormBox Lava</a> packs a punch but won’t break the bank. The sleek design means it’s easily portable, while water- and dustproofing equip it for any situation. The bass will shake the ground around you, but it still manages to produce a well-rounded sound with crisp treble.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2/" target="_blank" rel="nofollow" data-dimension112="c4f5f2fb-76f4-4803-8eb6-ee6b7125a3e3" data-action="Deal Block" data-label="StormBox Lava" data-dimension48="StormBox Lava" data-dimension25="$94">View Deal</a></p></div><div class="product"><a data-dimension112="68b15a7e-50f3-407f-939a-610865db8544" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$47" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1110px;"><p class="vanilla-image-block" style="padding-top:118.65%;"><img id="dsAxZA3JpSDwfF643VP3eB" name="Micro 3 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/dsAxZA3JpSDwfF643VP3eB.jpg" mos="" align="middle" fullscreen="" width="1110" height="1317" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review" data-dimension112="68b15a7e-50f3-407f-939a-610865db8544" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$47">Tribit StormBox Micro 3 </a>offers an IP68 rating, a companion app for EQ customization (although some settings cause compression), and simple controls that make the speaker a breeze to use. It has 24 hours of battery life and can even be used as a portable charger. But most importantly, the audio performance really is excellent.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S/" target="_blank" rel="nofollow" data-dimension112="68b15a7e-50f3-407f-939a-610865db8544" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$47">View Deal</a></p></div><div class="product"><a data-dimension112="d1e57288-124a-4492-a264-515519343437" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$27" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1480px;"><p class="vanilla-image-block" style="padding-top:101.35%;"><img id="iRvkqAH3YBcTTf4zRa7AdC" name="Tribit-StormBox-Mini-Plus-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/iRvkqAH3YBcTTf4zRa7AdC.jpg" mos="" align="middle" fullscreen="" width="1480" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review" data-dimension112="d1e57288-124a-4492-a264-515519343437" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$27">Tribit StormBox Mini+</a> boasts powerful bass response and detailed vocals. It also sports reactive LED lighting that’s fun to watch, and IPX7 waterproofing so it can be used poolside or in the shower. For the money, I recommend buying two so you can take advantage of the dreamy stereo sound.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Bluetooth-360%C2%B0Surround-Waterproof/dp/B0DSPCW5M5/" target="_blank" rel="nofollow" data-dimension112="d1e57288-124a-4492-a264-515519343437" data-action="Deal Block" data-label="StormBox Mini+" data-dimension48="StormBox Mini+" data-dimension25="$27">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-miscellaneous-deals"><span>Best Prime Day miscellaneous deals</span></h2><div class="product"><a data-dimension112="130232fe-27d8-4ce2-98e7-3ce3c65377d1" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$52" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1115px;"><p class="vanilla-image-block" style="padding-top:49.42%;"><img id="U4eB79RfdkjMCrjY6xNuFm" name="EarFun-UBoomL-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/U4eB79RfdkjMCrjY6xNuFm.png" mos="" align="middle" fullscreen="" width="1115" height="551" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/speakers/bluetooth-speakers/earfun-uboom-l-review" data-dimension112="130232fe-27d8-4ce2-98e7-3ce3c65377d1" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$52">EarFun UBoom L</a> offers big sound on a budget. This portable speaker produces powerful bass, clear vocals and well-balanced sound across a range of genres. With a dedicated outdoor mode and an IP67 resistance rating, the speaker can soundtrack pool parties and hikes. It has great battery life, and the customizable EQ via the user-friendly companion app works really well.<a class="view-deal button" href="https://www.amazon.com/Bluetooth-EarFun-Portable-Waterproof-Dustproof/dp/B0B1PJ5SQ7/" target="_blank" rel="nofollow" data-dimension112="130232fe-27d8-4ce2-98e7-3ce3c65377d1" data-action="Deal Block" data-label="UBoom L" data-dimension48="UBoom L" data-dimension25="$52">View Deal</a></p></div><div class="product"><a data-dimension112="549ce9e6-839f-4f0e-bda8-e1af2c057c61" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:892px;"><p class="vanilla-image-block" style="padding-top:116.82%;"><img id="r5zUUMfK2PzaubVDbQfHCF" name="8BitDo-RetroCube2-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/r5zUUMfK2PzaubVDbQfHCF.jpg" mos="" align="middle" fullscreen="" width="892" height="1042" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review" data-dimension112="549ce9e6-839f-4f0e-bda8-e1af2c057c61" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49">8BitDo Retro Cube 2</a> is the quirkiest speaker around, and that’s down to its NES-inspired design and D-pad-style controls. The speaker comes with a wireless charging dock with a slot to store the 2.4GHz dongle safely, and it boasts class-leading battery life of 30 hours. Crisp treble and a refined midrange make the speaker shine.<a class="view-deal button" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow" data-dimension112="549ce9e6-839f-4f0e-bda8-e1af2c057c61" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49">View Deal</a></p></div><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-speaker-deals"><span>We're tracking all the best Prime Day speaker deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Speakers&min_discount_ratio=0.95&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }        .tg-df-container .tg-df-carousel-scroll-left,    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-left { left: 8px; }    .tg-df-container .tg-df-carousel-scroll-right { right: 8px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left { left: 0px; }    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { right: 0px; }    .tg-df-carousel-filters-outer { margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; }    @container tg-df (max-width: 599px) { .tg-df-carousel-filters-outer { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; } }        .tg-df-container .tg-df-carousel-scroll-left:hover,    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left,    .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right {      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right { right: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-left { left: 0; }    .tg-df-grid-wrapper .tg-df-carousel-scroll-right { right: 0; }        .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left:hover,    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-outer" style="position: relative;">          \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>          \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>        \x3C/div>        \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3C/div>    \x3C/div>    \x3C/div>      \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button>      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>      \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }          const website = params.get('website') || 'tomsguide';          this.website = website;          if (website === 'techradar') {            const squadHeader = this.root.querySelector('.tg-df-savings-squad-header');            if (squadHeader) {               const pic = squadHeader.querySelector('picture');               if (pic) pic.style.display = 'none';            }            const style = document.createElement('style');            style.innerHTML = `              .tg-df-container .hawk-affiliate-link-deal-button { background-color: #5DAF08 !important; }              .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a8c06 !important; }            `;            this.root.appendChild(style);          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }          this.setupScrollListeners();        }        setupScrollListeners() {          const containers = [             this.root.querySelector('.tg-df-carousel-roundels'),             this.root.querySelector('.tg-df-carousel-filters-wrap'),             this.root.querySelector('#tg-df-grid')          ];                    containers.forEach(container => {             if (!container) return;                          const checkScroll = () => {                if (!container.parentElement) return;                const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left');                const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right');                                if (leftBtn) {                   if (container.scrollLeft <= 5) leftBtn.style.display = 'none';                   else leftBtn.style.display = 'flex';                }                                if (rightBtn) {                   if (container.scrollWidth <= container.clientWidth) {                       rightBtn.style.display = 'none';                   } else if (container.scrollLeft >= container.scrollWidth - container.clientWidth - 5) {                       rightBtn.style.display = 'none';                   } else {                       rightBtn.style.display = 'flex';                   }                }             };                          container.addEventListener('scroll', checkScroll);             checkScroll();                          window.addEventListener('resize', checkScroll);                          const observer = new MutationObserver(checkScroll);             observer.observe(container, { childList: true, subtree: true, characterData: false });          });        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          } else {             this.displayLimit += (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');             const encodedTag = encodeURIComponent(cleanTag);             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    if (this.rowsSelect && this.rowsSelect.value) {            url.searchParams.append('rows', this.rowsSelect.value);          } else {             url.searchParams.append('rows', '12'); // default          }          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = 12;          if (this.rowsSelect && this.rowsSelect.value) {            rowLimit = parseInt(this.rowsSelect.value, 10) || 12;          }          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map((v, idx) => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              const revenueIdVal = generateRevenueId(linkUrl, offerName, merchantName, null);              const rewrittenLinkUrl = rewriteAffiliateLink(linkUrl, area, revenueIdVal);              return `                \x3Ca href="${this.escapeHTML(rewrittenLinkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item"                  data-action="voucher-click"                  data-product-name="${this.escapeHTML(offerName)}"                  data-merchant-name="${this.escapeHTML(merchantName)}"                  data-analytics-id="${this.escapeHTML(offerObj.offer_id || offerObj.id || v.id || '')}"                  data-price=""                  data-previous-price=""                  data-original-link="${this.escapeHTML(linkUrl)}"                  data-revenue-id="${revenueIdVal}"                  data-index="${idx}"                  data-total="${offers.length}"                  data-in-stock="true"                  data-currency="USD"                  data-model-id="${this.escapeHTML(offerObj.model_id || v.model_id || offerObj.id || v.id || '')}"                  data-merchant-id="${this.escapeHTML(offerObj.merchant_id || offerObj.merchant?.id || '')}"                >                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });            // Attach voucher click tracking            const voucherBtns = content.querySelectorAll('[data-action="voucher-click"]');            voucherBtns.forEach(btn => {              btn.addEventListener('click', (e) => {                if (e.target.closest('[data-action="copy-code"]')) return;                                const productName = btn.getAttribute('data-product-name');                const merchantNameAttr = btn.getAttribute('data-merchant-name');                const productId = btn.getAttribute('data-analytics-id');                const price = parseFloat(btn.getAttribute('data-price')) || null;                const prevPriceStr = btn.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = btn.getAttribute('data-original-link');                const rewrittenLink = btn.getAttribute('href');                const revenueId = btn.getAttribute('data-revenue-id');                const index = parseInt(btn.getAttribute('data-index'), 10) || 0;                const inStock = btn.getAttribute('data-in-stock') === 'true';                const totalText = btn.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: btn.getAttribute('data-model-id') || null,                    matchId: btn.getAttribute('data-match-id') || null,                    brand: btn.getAttribute('data-model-brand') || null,                    parent: btn.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: btn.getAttribute('data-merchant-id') || null,                    network: btn.getAttribute('data-merchant-network') || null,                    url: btn.getAttribute('data-merchant-url') || null,                    name: merchantNameAttr                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: btn.getAttribute('data-currency') || 'USD'                };                if (typeof trackDealClick === 'function') {                  trackDealClick(trackingParams);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;          // Inject JSON-LD          try {            let targetNode = this.hostContainer || document.head;            let jsonLdScript = targetNode.querySelector('#tg-df-json-ld-' + this.widgetId);            if (!jsonLdScript) {                jsonLdScript = document.createElement('script');                jsonLdScript.type = 'application/ld+json';                jsonLdScript.id = 'tg-df-json-ld-' + this.widgetId;                targetNode.appendChild(jsonLdScript);            }            const jsonLdData = {              "@context": "https://schema.org",              "@type": "ItemList",              "itemListElement": displayDeals.slice(0, this.displayLimit).map((deal, index) => {                 let isoCurrency = "USD";                 if (deal.currency === '£') isoCurrency = "GBP";                 else if (deal.currency === '€') isoCurrency = "EUR";                 else if (deal.currency === 'A$') isoCurrency = "AUD";                 else if (deal.currency === 'CA$') isoCurrency = "CAD";                 const areaCode = typeof this.getAreaCode === 'function' ? this.getAreaCode() : 'US';                 const revenueId = typeof generateRevenueId === 'function' ? generateRevenueId(deal.url, deal.title, deal.merchant, null) : '';                 const rewrittenLink = typeof rewriteAffiliateLink === 'function' ? rewriteAffiliateLink(deal.url, areaCode, revenueId) : deal.url;                 return {                   "@type": "ListItem",                   "position": index + 1,                   "item": {                     "@type": "Product",                     "name": deal.title,                     "image": deal.image || "",                     "description": deal.description || "",                     "brand": {                       "@type": "Brand",                       "name": deal.brand || ""                     },                     "offers": {                       "@type": "Offer",                       "priceCurrency": isoCurrency,                       "price": deal.rawPrice || 0,                       "url": rewrittenLink,                       "seller": {                         "@type": "Organization",                         "name": deal.merchant || ""                       }                     }                   }                 };              }).filter(item => item.item.name)            };            jsonLdScript.textContent = JSON.stringify(jsonLdData);          } catch(e) { console.warn("JSON-LD generation failed", e); }                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let rightChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             let leftChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-left');             if (this.getViewMode() === 'carousel') {                 // The observer set up in setupScrollListeners handles visibility.                 if (rightChevron) rightChevron.style.display = 'flex';                 if (leftChevron) leftChevron.style.display = 'none'; // reset correctly             } else {                 if (rightChevron) rightChevron.style.display = 'none';                 if (leftChevron) leftChevron.style.display = 'none';             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12);                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            if (!this.processArticleFired) {                  this.processArticleFired = true;                  document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));               }            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just tested one of the most immersive soundbars on the market — here's why the KEF XIO is one to watch (and you can save £200) ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Owning one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a> is the best way to upgrade your home cinema with very little headache, and there's zero competition. Although serious hi-fi nerds poo-poo soundbars, some people don't have the time, space, or patience to deal with AVRs, floorstanders, wall-mounted speakers, and the like. Sometimes a soundbar is all you want. </p><p>And boy, have I got the soundbar for you: the <a href="https://www.tomsguide.com/audio/soundbars/kef-xio-review">KEF XIO</a>. The XIO is KEF's first foray into the home theater market, and it's a bullseye. Almost everything about this soundbar is excellent: the sound is seriously cinematic, it has powerful bass, and ridiculous Dolby Atmos. The main downside is the app, but if you're not the kind of person who uses soundbar apps, this will be a non-issue. </p><p>Here's why I recommend the KEF XIO for everyone willing to invest in their home sound — and how you can save £200/20% right now. </p><h2 id="sound-as-immersive-as-a-movie-theater">Sound as immersive as a movie theater</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xyX3GxJw7T547tu95wz2o3" name="KEF XIO 9.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/xyX3GxJw7T547tu95wz2o3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO is one of the most impressive soundbars I've ever tested, sonically. It has enviable width and height and makes movies come alive. I tested the soundbar with 4K UHD Blu-rays and standard 5.1 content on streaming services, and everything sounded excellent. </p><p>In my 4-star review, I wrote, "Dolby Atmos performance is utterly fantastic, especially in "Top Gun: Maverick". Every single flying scene had unbeatable height and width, and the planes seemed to fly over my head. I was incredibly impressed with this." </p><p>Certain movies were just <em>made</em> to be used with soundbars like this, and "Top Gun: Maverick" is one of them. If you grab this soundbar, you will not be disappointed if you use it with the right media. </p><p>I added, "Flying scenes sounded outrageously good; the bass rumbled in my chest when the jets took off". I could feel the planes moving around the room, could feel the engines in my <em>chest, </em>could hear the radio dialogue coming from behind me. I was seriously, seriously impressed with this soundbar. </p><div class="product"><a data-dimension112="7fd97380-1d37-4db6-87f6-aa1042e93af2" data-action="Deal Block" data-label="XIO" data-dimension48="XIO" data-dimension25="£1999" href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:40.00%;"><img id="tyJmm5KkgWA4VzjmiHoS4c" name="image" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/tyJmm5KkgWA4VzjmiHoS4c.png" mos="" align="middle" fullscreen="" width="1500" height="600" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The KEF XIO is £1,999 from Richer Sounds — and if you buy before 2nd July (and register with KEF, instructions below!) you can get £200 back. <a class="view-deal button" href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow" data-dimension112="7fd97380-1d37-4db6-87f6-aa1042e93af2" data-action="Deal Block" data-label="XIO" data-dimension48="XIO" data-dimension25="£1999">View Deal</a></p></div><p>The KEF XIO has a 5.1.2 channel configuration, which means it has five front- and up-firing speakers, one built-in subwoofer, and two side-firing speakers (that act as surrounds). This configuration is what gives that genuine Dolby Atmos performance — you want at least 3.1.2. </p><p>Another reason why the XIO sounds so good is its built-in, automatic calibration. KEF uses Intelligent Placement Technology built into the XIO. The soundbar uses microphones to map out your room (imagine a robot vacuum mapping out its territory) and then optimizes its sound based on things like placement height, wall size, and ceiling height. </p><p>Automatic calibration is a must-have if you want to get the very best out of your soundbar: the <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300x-review">JBL Bar 1300X</a> is also capable of this, as is <a href="https://www.tomsguide.com/audio/soundbars/klipsch-flexus-core-100-review">Klipsch's</a> Flexus line. </p><h2 id="save-200-if-you-re-in-the-u-k">Save £200 if you're in the U.K.</h2><p>For my fellow Brits, you can save <a href="https://uk.kef.com/pages/cashback" target="_blank" rel="nofollow">£200 on the KEF XIO right now</a>. It's not a <em>discount</em>, per se; more of a long-term reward. If you purchase before July 2nd, you get £200 cashback. </p><p>If you're in the U.S., you can save when you buy the <a href="https://us.kef.com/pages/xio-bundles" target="_blank" rel="nofollow">XIO with a subwoofer and receiver — so a $3,399 purchase becomes $2,679</a>. This isn't as good a deal as the U.K.'s — what if you don't want the subwoofer and receiver? — but I'll be on the lookout for Amazon deals. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ELHAbt4ZdScUC4GfGzuijE" name="KEF XIO 7.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/ELHAbt4ZdScUC4GfGzuijE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>KEF is also offering this discount for the following: Kube 8 MIE, Kube 10 MIE, Kube 12 MIE, KC62, but I haven't used these subwoofers, so I can't vouch for them. I can, however, vouch for the XIO — it is one of the most immersive, room-filling soundbars I've ever had the pleasure of testing. </p><p>Every sound effect was crystal-clear, every line of dialogue was pronounced and audible. As I've discussed above, the Dolby Atmos performance was seriously impressive and made me feel like I was at the movies.</p><p>If you can afford it, you won't be disappointed by the KEF XIO. It officially stole my heart.</p><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-soundbar-deals"><span>We're tracking all the best Prime Day soundbar deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Soundbars&min_discount_ratio=0.95&retailer=Amazon&rows=4&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&show_countdown=true&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right {      right: 0;      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>\n        \x3C/div>\n      \x3C/div>\n    \x3C/div>          \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          } else {             this.displayLimit += (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const encodedTag = encodeURIComponent(this.activeDealTag.toLowerCase().replace(/\s+/g, '-'));             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    if (this.rowsSelect && this.rowsSelect.value) {            url.searchParams.append('rows', this.rowsSelect.value);          } else {             url.searchParams.append('rows', '12'); // default          }          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = 12;          if (this.rowsSelect && this.rowsSelect.value) {            rowLimit = parseInt(this.rowsSelect.value, 10) || 12;          }          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map(v => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              return `                \x3Ca href="${this.escapeHTML(linkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item">                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let existingChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             if (this.getViewMode() === 'carousel') {                 if (!existingChevron) {                     gridWrapper.insertAdjacentHTML('beforeend', '\n                 \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: \'smooth\'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>');                 }             } else {                 if (existingChevron) {                     existingChevron.remove();                 }             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12);                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            if (!this.processArticleFired) {                  this.processArticleFired = true;                  document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));               }            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/i-just-tested-one-of-the-most-immersive-soundbars-on-the-market-heres-why-the-kef-xio-is-one-to-watch-and-you-can-save-gbp200</link>
                                                                            <description>
                            <![CDATA[ I reviewed the KEF XIO soundbar last week and felt like I was at the movies: here's why it's such a ferocious soundbar. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">LRAuHXPM29EDxRZWMjW4jJ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/RYDBRuxnsa8JugVAAv4SWm-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 23 Jun 2026 06:29:21 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/RYDBRuxnsa8JugVAAv4SWm-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the kef xio with the leg g5 and a blue tom&#039;s guide deal detective saving squad badge]]></media:description>                                                            <media:text><![CDATA[the kef xio with the leg g5 and a blue tom&#039;s guide deal detective saving squad badge]]></media:text>
                                <media:title type="plain"><![CDATA[the kef xio with the leg g5 and a blue tom&#039;s guide deal detective saving squad badge]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/RYDBRuxnsa8JugVAAv4SWm-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Owning one of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a> is the best way to upgrade your home cinema with very little headache, and there's zero competition. Although serious hi-fi nerds poo-poo soundbars, some people don't have the time, space, or patience to deal with AVRs, floorstanders, wall-mounted speakers, and the like. Sometimes a soundbar is all you want. </p><p>And boy, have I got the soundbar for you: the <a href="https://www.tomsguide.com/audio/soundbars/kef-xio-review">KEF XIO</a>. The XIO is KEF's first foray into the home theater market, and it's a bullseye. Almost everything about this soundbar is excellent: the sound is seriously cinematic, it has powerful bass, and ridiculous Dolby Atmos. The main downside is the app, but if you're not the kind of person who uses soundbar apps, this will be a non-issue. </p><p>Here's why I recommend the KEF XIO for everyone willing to invest in their home sound — and how you can save £200/20% right now. </p><h2 id="sound-as-immersive-as-a-movie-theater">Sound as immersive as a movie theater</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xyX3GxJw7T547tu95wz2o3" name="KEF XIO 9.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/xyX3GxJw7T547tu95wz2o3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO is one of the most impressive soundbars I've ever tested, sonically. It has enviable width and height and makes movies come alive. I tested the soundbar with 4K UHD Blu-rays and standard 5.1 content on streaming services, and everything sounded excellent. </p><p>In my 4-star review, I wrote, "Dolby Atmos performance is utterly fantastic, especially in "Top Gun: Maverick". Every single flying scene had unbeatable height and width, and the planes seemed to fly over my head. I was incredibly impressed with this." </p><p>Certain movies were just <em>made</em> to be used with soundbars like this, and "Top Gun: Maverick" is one of them. If you grab this soundbar, you will not be disappointed if you use it with the right media. </p><p>I added, "Flying scenes sounded outrageously good; the bass rumbled in my chest when the jets took off". I could feel the planes moving around the room, could feel the engines in my <em>chest, </em>could hear the radio dialogue coming from behind me. I was seriously, seriously impressed with this soundbar. </p><div class="product"><a data-dimension112="7fd97380-1d37-4db6-87f6-aa1042e93af2" data-action="Deal Block" data-label="XIO" data-dimension48="XIO" data-dimension25="£1999" href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:40.00%;"><img id="tyJmm5KkgWA4VzjmiHoS4c" name="image" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/tyJmm5KkgWA4VzjmiHoS4c.png" mos="" align="middle" fullscreen="" width="1500" height="600" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The KEF XIO is £1,999 from Richer Sounds — and if you buy before 2nd July (and register with KEF, instructions below!) you can get £200 back. <a class="view-deal button" href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow" data-dimension112="7fd97380-1d37-4db6-87f6-aa1042e93af2" data-action="Deal Block" data-label="XIO" data-dimension48="XIO" data-dimension25="£1999">View Deal</a></p></div><p>The KEF XIO has a 5.1.2 channel configuration, which means it has five front- and up-firing speakers, one built-in subwoofer, and two side-firing speakers (that act as surrounds). This configuration is what gives that genuine Dolby Atmos performance — you want at least 3.1.2. </p><p>Another reason why the XIO sounds so good is its built-in, automatic calibration. KEF uses Intelligent Placement Technology built into the XIO. The soundbar uses microphones to map out your room (imagine a robot vacuum mapping out its territory) and then optimizes its sound based on things like placement height, wall size, and ceiling height. </p><p>Automatic calibration is a must-have if you want to get the very best out of your soundbar: the <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300x-review">JBL Bar 1300X</a> is also capable of this, as is <a href="https://www.tomsguide.com/audio/soundbars/klipsch-flexus-core-100-review">Klipsch's</a> Flexus line. </p><h2 id="save-200-if-you-re-in-the-u-k">Save £200 if you're in the U.K.</h2><p>For my fellow Brits, you can save <a href="https://uk.kef.com/pages/cashback" target="_blank" rel="nofollow">£200 on the KEF XIO right now</a>. It's not a <em>discount</em>, per se; more of a long-term reward. If you purchase before July 2nd, you get £200 cashback. </p><p>If you're in the U.S., you can save when you buy the <a href="https://us.kef.com/pages/xio-bundles" target="_blank" rel="nofollow">XIO with a subwoofer and receiver — so a $3,399 purchase becomes $2,679</a>. This isn't as good a deal as the U.K.'s — what if you don't want the subwoofer and receiver? — but I'll be on the lookout for Amazon deals. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ELHAbt4ZdScUC4GfGzuijE" name="KEF XIO 7.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/ELHAbt4ZdScUC4GfGzuijE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>KEF is also offering this discount for the following: Kube 8 MIE, Kube 10 MIE, Kube 12 MIE, KC62, but I haven't used these subwoofers, so I can't vouch for them. I can, however, vouch for the XIO — it is one of the most immersive, room-filling soundbars I've ever had the pleasure of testing. </p><p>Every sound effect was crystal-clear, every line of dialogue was pronounced and audible. As I've discussed above, the Dolby Atmos performance was seriously impressive and made me feel like I was at the movies.</p><p>If you can afford it, you won't be disappointed by the KEF XIO. It officially stole my heart.</p><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-soundbar-deals"><span>We're tracking all the best Prime Day soundbar deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Soundbars&min_discount_ratio=0.95&retailer=Amazon&rows=4&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&show_countdown=true&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right {      right: 0;      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>\n        \x3C/div>\n      \x3C/div>\n    \x3C/div>          \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          } else {             this.displayLimit += (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const encodedTag = encodeURIComponent(this.activeDealTag.toLowerCase().replace(/\s+/g, '-'));             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    if (this.rowsSelect && this.rowsSelect.value) {            url.searchParams.append('rows', this.rowsSelect.value);          } else {             url.searchParams.append('rows', '12'); // default          }          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = 12;          if (this.rowsSelect && this.rowsSelect.value) {            rowLimit = parseInt(this.rowsSelect.value, 10) || 12;          }          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map(v => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              return `                \x3Ca href="${this.escapeHTML(linkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item">                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let existingChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             if (this.getViewMode() === 'carousel') {                 if (!existingChevron) {                     gridWrapper.insertAdjacentHTML('beforeend', '\n                 \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: \'smooth\'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>');                 }             } else {                 if (existingChevron) {                     existingChevron.remove();                 }             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12);                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            if (!this.processArticleFired) {                  this.processArticleFired = true;                  document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));               }            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Samsung, Sony, Bose, and Sonos have already launched Prime Day sales: Our highest-rated soundbars are up to $228 off right now ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I've sifted through the dregs of Amazon so you don't have to — <em>and</em> I've found savings for five of my highest-rated soundbars. As one of Tom's Guide's resident soundbar reviewers, I've tested enough of these home cinema bad boys to know exactly which ones are worth your time (and money). </p><p>It's not even Prime Day yet, and Samsung, Sony, <em>and</em> Bose have all slashed prices on some of their <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a>, including the super-budget <a href="https://www.tomsguide.com/audio/soundbars/sony-ht-s100f-review">Sony HT-S100F</a>, <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review">Bose Smart Soundbar</a> (usually $550!), and my personal favorite mid-range soundbar, <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>. I genuinely didn't believe this price when I saw it — <em>$371. </em>For a — really good — soundbar and subwoofer? Its launch price was $599! Is this real life? </p><p>I wouldn't wait for Prime Day with deals like this, but if you want to stick around for the Tom's Guide Savings Squad to save the day, we've selected the <a href="https://www.tomsguide.com/sales-events/prime-day-sneak-peek-ive-picked-41-amazon-deals-too-good-to-wait-for">best Prime Day deals</a> on everything from TVs to headphones to coffee makers to smart watches. </p><h2 class="article-body__section" id="section-best-early-prime-day-soundbar-deals-quick-links"><span>Best early Prime Day soundbar deals: Quick links</span></h2><ul><li><strong>Sony HT-S100F: </strong><a href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" target="_blank" rel="nofollow"><strong>was $118 now $98 @ Amazon </strong></a></li><li><strong>Klipsch Flexus Core 100: </strong><a href="https://www.amazon.com/Klipsch-100-Channel-Powered-Bluetooth/dp/B0CV5ZMDYY" target="_blank" rel="nofollow"><strong>was $349 now $249 @ Amazon</strong></a></li><li><strong>Sonos Beam Gen 2: </strong><a href="https://www.amazon.com/Sonos-Beam-Gen-2-Black/dp/B09GPYL7BJ" target="_blank" rel="nofollow"><strong>was $499 now $369 @ Amazon</strong></a></li><li><strong>Samsung Q600F: </strong><a href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8" target="_blank" rel="nofollow"><strong>was $599 now $371 @ Amazon</strong></a></li><li><strong>Bose Smart Soundbar: </strong><a href="https://www.amazon.com/Bose-Soundbar-Bluetooth-Assistant-Capabilities/dp/B0D6W8XQ6J" target="_blank" rel="nofollow"><strong>was $549 now $399 @ Amazon</strong></a></li></ul><h2 class="article-body__section" id="section-best-early-prime-day-soundbar-deals"><span>Best early Prime Day soundbar deals</span></h2><div class="product"><a data-dimension112="2d3e71b2-e7eb-47cd-af87-012cd3d2c86d" data-action="Deal Block" data-label="HT-S100F" data-dimension48="HT-S100F" data-dimension25="$98" href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="PMZLt3HzCP9i43jUL74LZS" name="Sony-Soundbar" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/PMZLt3HzCP9i43jUL74LZS.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/soundbars/sony-ht-s100f-review" data-dimension112="2d3e71b2-e7eb-47cd-af87-012cd3d2c86d" data-action="Deal Block" data-label="HT-S100F" data-dimension48="HT-S100F" data-dimension25="$98">Sony HT-S100F</a> (also called simply Sony S100F) is the best budget soundbar you can get nowadays, and there's very little competition. At launch, this soundbar was $99, but now it's usually around $119. Even so, when you can find it back at its pre-tariff price, it's definitely worth the investment. You get super clear dialogue and immersive sound for under $100.<a class="view-deal button" href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" target="_blank" rel="nofollow" data-dimension112="2d3e71b2-e7eb-47cd-af87-012cd3d2c86d" data-action="Deal Block" data-label="HT-S100F" data-dimension48="HT-S100F" data-dimension25="$98">View Deal</a></p></div><div class="product"><a data-dimension112="ce99bb02-06c5-4714-b09c-3d1cc489f81a" data-action="Deal Block" data-label="Flexus Core 100" data-dimension48="Flexus Core 100" data-dimension25="$249" href="https://www.amazon.com/Klipsch-100-Channel-Powered-Bluetooth/dp/B0CV5ZMDYY" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="Kst3T7bzZk2GeVeyZe7e5j" name="klipsch flexus core 100 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Kst3T7bzZk2GeVeyZe7e5j.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/soundbars/klipsch-flexus-core-100-review" data-dimension112="ce99bb02-06c5-4714-b09c-3d1cc489f81a" data-action="Deal Block" data-label="Flexus Core 100" data-dimension48="Flexus Core 100" data-dimension25="$249">Klipsch Flexus Core 100</a> is one of the best soundbars if you want to build out a whole sound system. You can start with Klipsch's entry-level soundbar (this one) and slowly work your way up with a sub and rears. The Core 100 has powerful bass and clear dialogue, making it ideal for movie nights and also reality TV binging. <a class="view-deal button" href="https://www.amazon.com/Klipsch-100-Channel-Powered-Bluetooth/dp/B0CV5ZMDYY" target="_blank" rel="nofollow" data-dimension112="ce99bb02-06c5-4714-b09c-3d1cc489f81a" data-action="Deal Block" data-label="Flexus Core 100" data-dimension48="Flexus Core 100" data-dimension25="$249">View Deal</a></p></div><div class="product"><a data-dimension112="0e7ef59b-205f-4723-b48e-37bb6aa6fa4d" data-action="Deal Block" data-label="Beam Gen 2" data-dimension48="Beam Gen 2" data-dimension25="$369" href="https://www.amazon.com/Sonos-Beam-Gen-2-Black/dp/B09GPYL7BJ" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="DAZRapi9fiu7HcN3m39JNd" name="Sonos Beam Gen 2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/DAZRapi9fiu7HcN3m39JNd.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>While I haven't personally reviewed the <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2" data-dimension112="0e7ef59b-205f-4723-b48e-37bb6aa6fa4d" data-action="Deal Block" data-label="Beam Gen 2" data-dimension48="Beam Gen 2" data-dimension25="$369">Sonos Beam Gen 2</a>, we gave it a lovely 4-star review when it first released in 2021. Even though this model is 5 years old, it still holds its own and is ideal for smaller TVs. It has fantastic Dolby Atmos and punchy bass, which makes it sound more expensive than it is. <a class="view-deal button" href="https://www.amazon.com/Sonos-Beam-Gen-2-Black/dp/B09GPYL7BJ" target="_blank" rel="nofollow" data-dimension112="0e7ef59b-205f-4723-b48e-37bb6aa6fa4d" data-action="Deal Block" data-label="Beam Gen 2" data-dimension48="Beam Gen 2" data-dimension25="$369">View Deal</a></p></div><div class="product"><a data-dimension112="8cf0b3b9-d67e-436a-82ea-122584c6fb1e" data-action="Deal Block" data-label="Q600F" data-dimension48="Q600F" data-dimension25="$371" href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="ExfR2TSGT2maYw6G6JSFS5" name="Samsung Q600F" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ExfR2TSGT2maYw6G6JSFS5.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I loved the <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review" data-dimension112="8cf0b3b9-d67e-436a-82ea-122584c6fb1e" data-action="Deal Block" data-label="Q600F" data-dimension48="Q600F" data-dimension25="$371">Samsung Q600F</a> when I reviewed it last year. This soundbar + subwoofer was <em>already</em> affordable at its launch price of $599, but now it's almost incomprehensibly cheap at $371. I'm not sure why the Amazon page lists its MSRP as $399 — it is most definitely $599. Although this is last year's model now — Samsung refreshes its soundbar lineup every year — I can assure you it sounds fantastic. You get immersive Dolby Atmos and crystal-clear dialogue for just $371... I know I'd be getting this one. <a class="view-deal button" href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8" target="_blank" rel="nofollow" data-dimension112="8cf0b3b9-d67e-436a-82ea-122584c6fb1e" data-action="Deal Block" data-label="Q600F" data-dimension48="Q600F" data-dimension25="$371">View Deal</a></p></div><div class="product"><a data-dimension112="9d784100-33db-4ba0-8dff-b6ba1832912f" data-action="Deal Block" data-label="Smart Soundbar" data-dimension48="Smart Soundbar" data-dimension25="$399" href="https://www.amazon.com/Bose-Soundbar-Bluetooth-Assistant-Capabilities/dp/B0D6W8XQ6J" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="XT9aaiGZoDf6MtibTHXgr6" name="bose smart soundbar" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XT9aaiGZoDf6MtibTHXgr6.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I have the <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review" data-dimension112="9d784100-33db-4ba0-8dff-b6ba1832912f" data-action="Deal Block" data-label="Smart Soundbar" data-dimension48="Smart Soundbar" data-dimension25="$399">Bose Smart Soundbar</a> at home and I love it. This compact soundbar is ideal for small living rooms without skimping on sound quality. Music playback is surprisingly great and AI Dialogue Mode means I never have to rewind due to missed speech. <a class="view-deal button" href="https://www.amazon.com/Bose-Soundbar-Bluetooth-Assistant-Capabilities/dp/B0D6W8XQ6J" target="_blank" rel="nofollow" data-dimension112="9d784100-33db-4ba0-8dff-b6ba1832912f" data-action="Deal Block" data-label="Smart Soundbar" data-dimension48="Smart Soundbar" data-dimension25="$399">View Deal</a></p></div><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-soundbar-deals"><span>We're tracking all the best Prime Day soundbar deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Soundbars&min_discount_ratio=0.95&retailer=Amazon&rows=4&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&show_countdown=true&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right {      right: 0;      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>\n        \x3C/div>\n      \x3C/div>\n    \x3C/div>          \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          } else {             this.displayLimit += (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const encodedTag = encodeURIComponent(this.activeDealTag.toLowerCase().replace(/\s+/g, '-'));             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    if (this.rowsSelect && this.rowsSelect.value) {            url.searchParams.append('rows', this.rowsSelect.value);          } else {             url.searchParams.append('rows', '12'); // default          }          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = 12;          if (this.rowsSelect && this.rowsSelect.value) {            rowLimit = parseInt(this.rowsSelect.value, 10) || 12;          }          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map(v => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              return `                \x3Ca href="${this.escapeHTML(linkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item">                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let existingChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             if (this.getViewMode() === 'carousel') {                 if (!existingChevron) {                     gridWrapper.insertAdjacentHTML('beforeend', '\n                 \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: \'smooth\'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>');                 }             } else {                 if (existingChevron) {                     existingChevron.remove();                 }             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12);                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            if (!this.processArticleFired) {                  this.processArticleFired = true;                  document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));               }            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/samsung-sony-bose-and-sonos-have-already-launched-prime-day-sales-our-highest-rated-soundbars-are-up-to-usd228-off-right-now</link>
                                                                            <description>
                            <![CDATA[ I wouldn't wait until Prime Day for these soundbar deals. I've found Bose, Sony, Samsung, Klipsch and Sonos savings on some of their best soundbars. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">MYPyVEfXx2CE5ggb63gWsg</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/wmsvJwYR482seAibhPpppk-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 22 Jun 2026 09:15:12 +0000</pubDate>                                                                                                                                <updated>Mon, 22 Jun 2026 09:49:41 +0000</updated>
                                                                                                                                            <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/wmsvJwYR482seAibhPpppk-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the samsung q600f prime day hero image with the tom&#039;s guide savings squad badge]]></media:description>                                                            <media:text><![CDATA[the samsung q600f prime day hero image with the tom&#039;s guide savings squad badge]]></media:text>
                                <media:title type="plain"><![CDATA[the samsung q600f prime day hero image with the tom&#039;s guide savings squad badge]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/wmsvJwYR482seAibhPpppk-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I've sifted through the dregs of Amazon so you don't have to — <em>and</em> I've found savings for five of my highest-rated soundbars. As one of Tom's Guide's resident soundbar reviewers, I've tested enough of these home cinema bad boys to know exactly which ones are worth your time (and money). </p><p>It's not even Prime Day yet, and Samsung, Sony, <em>and</em> Bose have all slashed prices on some of their <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbars</a>, including the super-budget <a href="https://www.tomsguide.com/audio/soundbars/sony-ht-s100f-review">Sony HT-S100F</a>, <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review">Bose Smart Soundbar</a> (usually $550!), and my personal favorite mid-range soundbar, <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review">Samsung Q600F</a>. I genuinely didn't believe this price when I saw it — <em>$371. </em>For a — really good — soundbar and subwoofer? Its launch price was $599! Is this real life? </p><p>I wouldn't wait for Prime Day with deals like this, but if you want to stick around for the Tom's Guide Savings Squad to save the day, we've selected the <a href="https://www.tomsguide.com/sales-events/prime-day-sneak-peek-ive-picked-41-amazon-deals-too-good-to-wait-for">best Prime Day deals</a> on everything from TVs to headphones to coffee makers to smart watches. </p><h2 class="article-body__section" id="section-best-early-prime-day-soundbar-deals-quick-links"><span>Best early Prime Day soundbar deals: Quick links</span></h2><ul><li><strong>Sony HT-S100F: </strong><a href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" target="_blank" rel="nofollow"><strong>was $118 now $98 @ Amazon </strong></a></li><li><strong>Klipsch Flexus Core 100: </strong><a href="https://www.amazon.com/Klipsch-100-Channel-Powered-Bluetooth/dp/B0CV5ZMDYY" target="_blank" rel="nofollow"><strong>was $349 now $249 @ Amazon</strong></a></li><li><strong>Sonos Beam Gen 2: </strong><a href="https://www.amazon.com/Sonos-Beam-Gen-2-Black/dp/B09GPYL7BJ" target="_blank" rel="nofollow"><strong>was $499 now $369 @ Amazon</strong></a></li><li><strong>Samsung Q600F: </strong><a href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8" target="_blank" rel="nofollow"><strong>was $599 now $371 @ Amazon</strong></a></li><li><strong>Bose Smart Soundbar: </strong><a href="https://www.amazon.com/Bose-Soundbar-Bluetooth-Assistant-Capabilities/dp/B0D6W8XQ6J" target="_blank" rel="nofollow"><strong>was $549 now $399 @ Amazon</strong></a></li></ul><h2 class="article-body__section" id="section-best-early-prime-day-soundbar-deals"><span>Best early Prime Day soundbar deals</span></h2><div class="product"><a data-dimension112="2d3e71b2-e7eb-47cd-af87-012cd3d2c86d" data-action="Deal Block" data-label="HT-S100F" data-dimension48="HT-S100F" data-dimension25="$98" href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="PMZLt3HzCP9i43jUL74LZS" name="Sony-Soundbar" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/PMZLt3HzCP9i43jUL74LZS.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/soundbars/sony-ht-s100f-review" data-dimension112="2d3e71b2-e7eb-47cd-af87-012cd3d2c86d" data-action="Deal Block" data-label="HT-S100F" data-dimension48="HT-S100F" data-dimension25="$98">Sony HT-S100F</a> (also called simply Sony S100F) is the best budget soundbar you can get nowadays, and there's very little competition. At launch, this soundbar was $99, but now it's usually around $119. Even so, when you can find it back at its pre-tariff price, it's definitely worth the investment. You get super clear dialogue and immersive sound for under $100.<a class="view-deal button" href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" target="_blank" rel="nofollow" data-dimension112="2d3e71b2-e7eb-47cd-af87-012cd3d2c86d" data-action="Deal Block" data-label="HT-S100F" data-dimension48="HT-S100F" data-dimension25="$98">View Deal</a></p></div><div class="product"><a data-dimension112="ce99bb02-06c5-4714-b09c-3d1cc489f81a" data-action="Deal Block" data-label="Flexus Core 100" data-dimension48="Flexus Core 100" data-dimension25="$249" href="https://www.amazon.com/Klipsch-100-Channel-Powered-Bluetooth/dp/B0CV5ZMDYY" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="Kst3T7bzZk2GeVeyZe7e5j" name="klipsch flexus core 100 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Kst3T7bzZk2GeVeyZe7e5j.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/soundbars/klipsch-flexus-core-100-review" data-dimension112="ce99bb02-06c5-4714-b09c-3d1cc489f81a" data-action="Deal Block" data-label="Flexus Core 100" data-dimension48="Flexus Core 100" data-dimension25="$249">Klipsch Flexus Core 100</a> is one of the best soundbars if you want to build out a whole sound system. You can start with Klipsch's entry-level soundbar (this one) and slowly work your way up with a sub and rears. The Core 100 has powerful bass and clear dialogue, making it ideal for movie nights and also reality TV binging. <a class="view-deal button" href="https://www.amazon.com/Klipsch-100-Channel-Powered-Bluetooth/dp/B0CV5ZMDYY" target="_blank" rel="nofollow" data-dimension112="ce99bb02-06c5-4714-b09c-3d1cc489f81a" data-action="Deal Block" data-label="Flexus Core 100" data-dimension48="Flexus Core 100" data-dimension25="$249">View Deal</a></p></div><div class="product"><a data-dimension112="0e7ef59b-205f-4723-b48e-37bb6aa6fa4d" data-action="Deal Block" data-label="Beam Gen 2" data-dimension48="Beam Gen 2" data-dimension25="$369" href="https://www.amazon.com/Sonos-Beam-Gen-2-Black/dp/B09GPYL7BJ" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="DAZRapi9fiu7HcN3m39JNd" name="Sonos Beam Gen 2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/DAZRapi9fiu7HcN3m39JNd.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>While I haven't personally reviewed the <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2" data-dimension112="0e7ef59b-205f-4723-b48e-37bb6aa6fa4d" data-action="Deal Block" data-label="Beam Gen 2" data-dimension48="Beam Gen 2" data-dimension25="$369">Sonos Beam Gen 2</a>, we gave it a lovely 4-star review when it first released in 2021. Even though this model is 5 years old, it still holds its own and is ideal for smaller TVs. It has fantastic Dolby Atmos and punchy bass, which makes it sound more expensive than it is. <a class="view-deal button" href="https://www.amazon.com/Sonos-Beam-Gen-2-Black/dp/B09GPYL7BJ" target="_blank" rel="nofollow" data-dimension112="0e7ef59b-205f-4723-b48e-37bb6aa6fa4d" data-action="Deal Block" data-label="Beam Gen 2" data-dimension48="Beam Gen 2" data-dimension25="$369">View Deal</a></p></div><div class="product"><a data-dimension112="8cf0b3b9-d67e-436a-82ea-122584c6fb1e" data-action="Deal Block" data-label="Q600F" data-dimension48="Q600F" data-dimension25="$371" href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="ExfR2TSGT2maYw6G6JSFS5" name="Samsung Q600F" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ExfR2TSGT2maYw6G6JSFS5.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I loved the <a href="https://www.tomsguide.com/audio/soundbars/samsung-q600f-review" data-dimension112="8cf0b3b9-d67e-436a-82ea-122584c6fb1e" data-action="Deal Block" data-label="Q600F" data-dimension48="Q600F" data-dimension25="$371">Samsung Q600F</a> when I reviewed it last year. This soundbar + subwoofer was <em>already</em> affordable at its launch price of $599, but now it's almost incomprehensibly cheap at $371. I'm not sure why the Amazon page lists its MSRP as $399 — it is most definitely $599. Although this is last year's model now — Samsung refreshes its soundbar lineup every year — I can assure you it sounds fantastic. You get immersive Dolby Atmos and crystal-clear dialogue for just $371... I know I'd be getting this one. <a class="view-deal button" href="https://www.amazon.com/Samsung-Soundbar-HW-Q600F-Wireless-Subwoofer/dp/B0GS4P3YL8" target="_blank" rel="nofollow" data-dimension112="8cf0b3b9-d67e-436a-82ea-122584c6fb1e" data-action="Deal Block" data-label="Q600F" data-dimension48="Q600F" data-dimension25="$371">View Deal</a></p></div><div class="product"><a data-dimension112="9d784100-33db-4ba0-8dff-b6ba1832912f" data-action="Deal Block" data-label="Smart Soundbar" data-dimension48="Smart Soundbar" data-dimension25="$399" href="https://www.amazon.com/Bose-Soundbar-Bluetooth-Assistant-Capabilities/dp/B0D6W8XQ6J" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="XT9aaiGZoDf6MtibTHXgr6" name="bose smart soundbar" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XT9aaiGZoDf6MtibTHXgr6.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>I have the <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review" data-dimension112="9d784100-33db-4ba0-8dff-b6ba1832912f" data-action="Deal Block" data-label="Smart Soundbar" data-dimension48="Smart Soundbar" data-dimension25="$399">Bose Smart Soundbar</a> at home and I love it. This compact soundbar is ideal for small living rooms without skimping on sound quality. Music playback is surprisingly great and AI Dialogue Mode means I never have to rewind due to missed speech. <a class="view-deal button" href="https://www.amazon.com/Bose-Soundbar-Bluetooth-Assistant-Capabilities/dp/B0D6W8XQ6J" target="_blank" rel="nofollow" data-dimension112="9d784100-33db-4ba0-8dff-b6ba1832912f" data-action="Deal Block" data-label="Smart Soundbar" data-dimension48="Smart Soundbar" data-dimension25="$399">View Deal</a></p></div><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-soundbar-deals"><span>We're tracking all the best Prime Day soundbar deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Soundbars&min_discount_ratio=0.95&retailer=Amazon&rows=4&widget_title=Top+Deals+Handpicked+by+Our+Editors&widget_subtitle=Discover+the+best+discounts+currently+available%2C+curated+daily+by+the+Tom%27s+Guide+Savings+Squad.&show_countdown=true&bg_color=transparent" data-vizualizer-embed="true"></div>    <script>    /**     * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine     */    (function() {      // --- Freyr Analytics Adapter ---      function initAnalytics() {        window.dataLayer = window.dataLayer || [];        window.googletag = window.googletag || {};        window.googletag.cmd = window.googletag.cmd || [];        window.hawk = window.hawk || { analytics: { freyr: [] } };        window.hawk.analytics = window.hawk.analytics || { freyr: [] };        window.hawk.analytics.freyr = window.hawk.analytics.freyr || [];        window.freyr = window.freyr || { cmd: [] };        const scriptSrc = 'https://freyr.futurecdn.net/freyr.js';        const hostname = typeof window !== 'undefined' ? window.location.hostname : '';        const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless'));        const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app');        if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) {          const script = document.createElement('script');          script.src = scriptSrc;          script.async = true;          document.head.appendChild(script);        }      }      function storeEventForDebug(name, data) {        if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return;        window.hawk.analytics.freyr.push({ name, data });        try {          if (typeof window !== 'undefined' && window.localStorage) {            window.localStorage.setItem("hawk", JSON.stringify(window.hawk));          }        } catch (e) {          // Ignore storage issues        }        try {          window.dispatchEvent(new CustomEvent("hawk-analytics-update"));        } catch (e) {}      }      function sendToFreyr(eventName, data) {        if (typeof window === 'undefined') return;        window.freyr = window.freyr || { cmd: [] };        window.freyr.cmd.push(() => {          if (window.freyr && window.freyr.pushAndUpdate) {            window.freyr.pushAndUpdate(eventName, data);          }        });      }      function sendEvent(event, skip = false) {        try {          storeEventForDebug(event.name, event.data);          if (!skip) {            sendToFreyr(event.name, event.data);          }        } catch (e) {          // Ensure tracking errors don't surface to the user        }      }      function getCookie(name) {        try {          const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));          return match ? match[2] : null;        } catch (e) {          return null;        }      }      function normalizeCurrency(symbol) {        const map = {          '£': 'GBP',          '$': 'USD',          'A$': 'AUD',          'CA$': 'CAD',          '€': 'EUR'        };        return map[symbol] || symbol;      }      function trackElementInteraction(props) {        sendEvent({          name: 'elementInteraction',          data: {            element: {              action: props.action || "click",              id: props.id || undefined,              class: props.class || undefined,              name: props.name || undefined,              text: props.text || undefined,              label: props.label || undefined,              container: props.container || undefined,              url: props.url || undefined,              articleId: props.articleId || undefined            }          }        });      }      function generateRevenueId(url, productName, merchantName, modelId) {        const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`;        let hash = 0;        for (let i = 0; i < str.length; i++) {          const char = str.charCodeAt(i);          hash = ((hash << 5) - hash) + char;          hash = hash & hash;        }        let numericStr = Math.abs(hash).toString();        while (numericStr.length < 19) {          numericStr += Math.floor(Math.random() * 10).toString();        }        return numericStr.substring(0, 19);      }      function rewriteAffiliateLink(url, territory, revenueId) {        if (!url) return url;        const t = (territory || 'gb').toLowerCase();        return url.replace(/hawk-custom-tracking/g, `tomsguide-${t}-${revenueId}`);      }      function trackHawkEvent(params) {        const { clickType, widgetId, productCategoryName, product, productsArray, zeroBasedProductIndexOrNull, totalDealsOrProducts, areaClicked, merchant, revenueId, isoCurrencyCode, queryName, widgetTypeName } = params;        const data = {          event: "hawkEvent",          category: "Affiliates",          affiliate: {            action: {              type: clickType,              id: widgetId,              event: clickType === "appeared" ? "viewed" : "Click from",              timestamp: Date.now()            },            component: {              flag: "Editor",              product: productCategoryName || "deals",              category: `Signal Deal Finder ${widgetTypeName || "Carousel"} widget`,              type: clickType === "appeared" ? "review" : "signal product",              label: queryName || (product ? (product.name || "") : ""),              index: zeroBasedProductIndexOrNull === null || zeroBasedProductIndexOrNull === undefined ? -1 : zeroBasedProductIndexOrNull,              linkCount: totalDealsOrProducts || 0,              blockLayout: "",              areaClicked: areaClicked || ""            }          },          products: productsArray || (product && merchant ? [            {              product: {                primary: {                  id: product.id || product.matchId || null,                  name: product.name,                  type: "deal",                  price: product.price,                  previousPrice: product.previousPrice || null,                  currency: isoCurrencyCode || "USD",                  preorder: false,                  labels: [],                  link: product.link,                  originalLink: product.originalLink || null,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: null,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: product.globalId || null,                  inStock: product.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: isoCurrencyCode || "USD"                }              },              merchant: {                id: merchant.id || null,                name: merchant.name,                url: merchant.url || null,                network: merchant.network || null              },              model: {                id: product.modelId || null,                brand: product.brand || null,                name: product.name,                parent: product.parent || null              }            }          ] : []),          reviews: [],          _clear: true,          "gtm.uniqueEventId": Date.now() % 10000        };        sendEvent({ name: 'hawkEvent', data });      }      function trackDealClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card" });      }      function trackViewSimilarClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Product Card View Similar" });      }      function trackPriceComparisonClick(params) {        trackHawkEvent({ ...params, clickType: "retailer", areaClicked: "Signal Price Comparison" });      }      function trackReviewClick(params) {        trackHawkEvent({ ...params, clickType: "review", areaClicked: "Signal Product Card Review Link" });      }      function trackShare(params) {        trackHawkEvent({ ...params, clickType: "share", areaClicked: "Signal Product Card Share" });      }      function trackDealsAppeared(widgetId, deals, revenueId, currency, queryName, widgetTypeName) {         if (!deals || deals.length === 0) return;                  const productsArray = deals.slice(0, 50).map((deal) => {            let voucherPct = null;            let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null;            let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null;            if (rawMsrp > rawPrice && rawPrice > 0) {              voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100);            }            let numId = null;            if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) {              numId = parseInt(deal.externalProductId);            } else if (deal.id && !isNaN(parseInt(deal.id))) {              numId = parseInt(deal.id);            } else {              numId = deal.matchId || null;            }            return {              product: {                primary: {                  id: numId,                  name: deal.productName || deal.title || "",                  type: "deal",                  price: rawPrice,                  previousPrice: rawMsrp,                  currency: currency || 'USD',                  preorder: false,                  labels: deal.modelBrand || deal.brand ? [                     { type: "brand", value: deal.modelBrand || deal.brand }                  ] : [],                  link: deal.url,                  originalLink: deal.url,                  revenueId: revenueId || null,                  startTime: null,                  endTime: null,                  voucherCode: null,                  voucherAudience: null,                  voucherPercentageSaving: voucherPct,                  voucherMoneySaving: null,                  voucherType: null,                  offerExclusive: false,                  offerScope: null,                  globalId: deal.productKey || null,                  inStock: deal.inStock !== false,                  contractProvider: null,                  contractMinutes: null,                  contractTexts: null,                  contractData: null,                  contractLength: null,                  contractMonthlyPrice: null,                  contractCurrency: currency || 'USD'                }              },              merchant: {                id: deal.merchantId ? parseInt(deal.merchantId) : null,                name: deal.merchant || "Retailer",                url: deal.merchantUrl || null,                network: deal.merchantNetwork || null              },              model: {                id: deal.modelId ? parseInt(deal.modelId) : null,                brand: deal.modelBrand || deal.brand || null,                name: deal.productName || deal.title || "",                parent: deal.modelParent || null              }            };         });                  trackHawkEvent({             clickType: "appeared",             widgetId: widgetId,             productCategoryName: "deals",             zeroBasedProductIndexOrNull: null,             totalDealsOrProducts: deals.length,             productsArray: productsArray,             queryName: queryName,             widgetTypeName: widgetTypeName         });      }      // 1. Setup Shadow DOM Sandbox      const currentScript = document.currentScript;      let hostContainer = null;      let template = null;            if (currentScript) {        let prev = currentScript.previousElementSibling;        while (prev) {          if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) {            template = prev;          } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) {            hostContainer = prev;            break;          }          prev = prev.previousElementSibling;        }      }            // Fallbacks in case script is deferred      if (!hostContainer) {        const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])');        if (hosts.length > 0) hostContainer = hosts[0];      }            // Safely embedded template for CMS environments      const rawTemplate = `  \x3Cstyle>    /* --- Shadow DOM Base Reset --- */    *, *::before, *::after {      box-sizing: border-box;    }    img, picture, svg, video {      max-width: 100%;      height: auto;      display: block;    }    /*       1. Scoped CSS for Tom's Guide Deals Widget       All classes are prefixed with \`tg-df-\` to prevent CMS style leakage.    */    .tg-df-container {      container-type: inline-size;      container-name: tg-df;      --tg-df-blue: #1F69FF;      --tg-df-blue-hover: #004d8c;      --tg-df-text: #222222;      --tg-df-text-muted: #555555;      --tg-df-bg: #ffffff;      --tg-df-bg-secondary: #f4f4f4;      --tg-df-border: #e2e8f0;      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;      color: var(--tg-df-text);      background-color: transparent;       width: 100%;      max-width: 1200px;      margin: 0 auto;      padding-bottom: 24px;    }    .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after {      margin: 0;      padding: 0;      box-sizing: border-box;    }    .tg-df-container img {      border: none;      margin: 0;      padding: 0;    }    .tg-df-container a {      text-decoration: none;      color: inherit;    }    /*       2. Search & Filter Bar    */    .tg-df-controls {      display: flex;      flex-direction: column;      align-items: center;      gap: 20px;      margin-bottom: 32px;      width: 100%;    }    .tg-df-top-bar {      display: flex;      width: 100%;      max-width: 760px;      gap: 12px;      align-items: center;    }    .tg-df-search-wrapper {      position: relative;      flex: 1;      width: 100%;      box-shadow: 0 8px 24px rgba(0,0,0,0.06);      border-radius: 40px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      z-index: 100;    }    .tg-df-autocomplete-dropdown {      position: absolute;      top: calc(100% + 4px);      left: 0;      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      max-height: 300px;      overflow-y: auto;      z-index: 200;      display: none;    }    .tg-df-autocomplete-dropdown.active {      display: block;    }    .tg-df-autocomplete-item {      padding: 12px 24px;      cursor: pointer;      font-size: 14px;      color: var(--tg-df-text);      transition: background 0.1s ease;    }    .tg-df-autocomplete-item:hover {      background: var(--tg-df-bg-secondary);    }    .tg-df-search-input {      width: 100%;      padding: 16px 64px 16px 24px;      font-size: 16px;      border: 2px solid transparent;      border-radius: 40px;      outline: none;      transition: border-color 0.2s ease, box-shadow 0.2s ease;      color: var(--tg-df-text);      background: transparent;    }    .tg-df-search-input:focus {      border-color: transparent;      box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15);    }    .tg-df-search-input::placeholder {      color: #999999;    }        .tg-df-search-btn {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      width: 40px;      height: 40px;      border-radius: 50%;      background: #222;      border: none;      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: background 0.2s ease;    }        .tg-df-search-btn:hover {      background: #000;    }    .tg-df-search-icon {      width: 16px;      height: 16px;      fill: #fff;    }    .tg-df-settings-wrapper {      position: relative;    }        .tg-df-settings-btn {      width: 48px;      height: 48px;      border-radius: 50%;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      box-shadow: 0 4px 12px rgba(0,0,0,0.04);      display: flex;      align-items: center;      justify-content: center;      cursor: pointer;      transition: all 0.2s ease;      color: var(--tg-df-text-muted);      flex-shrink: 0;    }    .tg-df-settings-btn:hover {      background: var(--tg-df-bg-secondary);      border-color: #0000ff;      color: var(--tg-df-text);    }    .tg-df-settings-btn svg {      width: 24px;      height: 24px;      fill: currentColor;    }    .tg-df-settings-dropdown {      position: absolute;      top: calc(100% + 8px);      right: 0;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 12px;      box-shadow: 0 8px 32px rgba(0,0,0,0.12);      width: 280px;      padding: 20px;      display: none;      z-index: 100;      flex-direction: column;      gap: 20px;    }    .tg-df-settings-dropdown.active {      display: flex;    }        .tg-df-settings-dropdown-backdrop {      display: none;      position: fixed;      inset: 0;      z-index: 99;    }        .tg-df-settings-dropdown-backdrop.active {      display: block;    }    .tg-df-setting-item {      display: flex;      flex-direction: column;      gap: 10px;    }    .tg-df-setting-label {      font-size: 11px;      font-weight: 700;      color: var(--tg-df-text-muted);      text-transform: uppercase;      letter-spacing: 0.5px;    }        .tg-df-region-select {        padding: 10px 12px;        border-radius: 8px;        border: 1px solid var(--tg-df-border);        font-size: 15px;        outline: none;        background: var(--tg-df-bg-secondary);        color: var(--tg-df-text);        cursor: pointer;        width: 100%;    }    .tg-df-toggle {        position: relative;        display: inline-block;        width: 44px;        height: 24px;        flex-shrink: 0;    }    .tg-df-toggle input {        opacity: 0;        width: 0;        height: 0;    }    .tg-df-slider {        position: absolute;        cursor: pointer;        top: 0; left: 0; right: 0; bottom: 0;        background-color: #ccc;        transition: .2s;        border-radius: 24px;    }    .tg-df-slider:before {        position: absolute;        content: "";        height: 18px;        width: 18px;        left: 3px;        bottom: 3px;        background-color: white;        transition: .2s;        border-radius: 50%;    }    .tg-df-toggle input:checked + .tg-df-slider {        background-color: #1F69FF;    }    .tg-df-toggle input:checked + .tg-df-slider:before {        transform: translateX(20px);    }    .tg-df-dl-row {        flex-direction: row;        align-items: center;        justify-content: space-between;    }    .tg-df-dl-row-text {        font-size: 14px;        font-weight: 600;        color: var(--tg-df-text);    }    .tg-df-dl-row-subtext {        font-size: 12px;        font-weight: 400;        line-height: 1.3;        color: var(--tg-df-text-muted);        margin-top: 4px;        display: block;    }    .tg-df-filters {      display: flex;      gap: 12px;      justify-content: center;      flex-wrap: wrap;    }    .tg-df-sort-wrapper {      position: relative;      display: flex;      align-items: center;    }        .tg-df-sort-icon {      position: absolute;      left: 14px;      width: 14px;      height: 14px;      fill: var(--tg-df-text-muted);      pointer-events: none;    }    .tg-df-sort-select, .tg-df-filter-select {      padding: 10px 36px 10px 38px;      font-size: 14px;      border: 1px solid var(--tg-df-border);      border-radius: 100px;      outline: none;      appearance: none;      background-color: var(--tg-df-bg-secondary);      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E");      background-repeat: no-repeat;      background-position: right 14px center;      color: var(--tg-df-text);      cursor: pointer;      font-weight: 500;      transition: all 0.2s ease;    }        .tg-df-price-input::-webkit-outer-spin-button,    .tg-df-price-input::-webkit-inner-spin-button {      -webkit-appearance: none;      margin: 0;    }    .tg-df-price-input {      -moz-appearance: textfield;    }    .tg-df-sort-select:hover, .tg-df-filter-select:hover {      background-color: #e2e8f0;    }    .tg-df-multiselect-container {      position: relative;    }        .tg-df-multiselect-trigger {      display: block;      background: #fff;      user-select: none;      width: 100%;      overflow: hidden;      white-space: nowrap;      text-overflow: ellipsis;    }        .tg-df-multiselect-dropdown {      display: none;      position: absolute;      top: calc(100% + 4px);      left: 0;      width: 100%;      min-width: 220px;      max-height: 300px;      overflow-y: auto;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);      z-index: 100;      padding: 8px 0;    }    .tg-df-multiselect-dropdown.active {      display: block;    }    .tg-df-ms-option {      padding: 8px 16px;      display: flex;      align-items: center;      gap: 8px;      cursor: pointer;      font-size: 14px;    }    .tg-df-ms-option:hover {      background-color: var(--tg-df-bg-secondary);    }        .tg-df-ms-option input {      cursor: pointer;      accent-color: #1f69ff;    }    .tg-df-sort-select:focus, .tg-df-filter-select:focus {      border-color: #0000ff;      box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2);      background-color: var(--tg-df-bg);    }    /*       3. Deal Grid Layout    */    .tg-df-grid.tg-df-grid-auto {      padding-top: 24px;    }    .tg-df-grid, .tg-df-grid.layout-grid {      display: grid;      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));      gap: 10px;    }    .tg-df-grid.layout-row {      grid-template-columns: 1fr;      gap: 16px;    }        .tg-df-grid.layout-row .tg-df-card {      flex-direction: row;      align-items: stretch;      height: auto;      box-shadow: none;      border-bottom: 1px solid var(--tg-df-border);    }    .tg-df-grid.layout-row .tg-df-card:hover {      box-shadow: none;    }    .tg-df-grid.layout-row .tg-df-card-image-box {      width: 140px;      min-width: 140px;      aspect-ratio: 3/4;      border-right: none;      padding: 16px 16px 16px 32px;    }    .tg-df-grid.layout-row .tg-df-card-body {      padding: 16px;      justify-content: space-between;    }    .tg-df-grid.layout-row .tg-df-card-title {      font-size: 15px;      margin-bottom: 16px;    }    .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; }    .tg-df-grid.layout-row .tg-df-card-footer {      flex-direction: column;      align-items: flex-start;      gap: 0;    }    .tg-df-grid.layout-row .tg-df-card-merchant-pill {      margin-bottom: 4px;    }    .tg-df-grid.layout-row .tg-df-card-price-group {      margin-bottom: 8px;    }    .tg-df-grid.layout-row .tg-df-price-group {      width: auto;    }    .tg-df-grid.layout-row .tg-df-card-cta {      width: 100%;      max-width: 200px;      padding: 10px 24px;      font-size: 13px;      flex-shrink: 0;      text-align: center;      justify-content: center;    }    /*       4. Deal Card Design    */    .tg-df-card {      position: relative;      display: flex;      flex-direction: column;      background-color: #ffffff;      border-radius: 0;      overflow: hidden;      transition: transform 0.2s ease, box-shadow 0.2s ease;      text-decoration: none;      color: inherit;      height: 100%;      box-shadow: 0 0 16px rgba(0, 0, 0, 0.08);      border: 1px solid var(--tg-df-border);    }    .tg-df-card:hover {      box-shadow: 0 0 24px rgba(0, 0, 0, 0.12);    }    .tg-df-card-image-box {      width: 100%;      aspect-ratio: 3/4;      background-color: #f8f8f8;      display: flex;      align-items: center;      justify-content: center;      position: relative;      overflow: hidden;      padding: 32px;      flex: 0 0 auto;    }    .tg-df-card-image {      max-width: 100%;      max-height: 100%;      width: auto;      height: auto;      object-fit: contain;      mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */      transition: transform 0.3s ease;    }    .tg-df-card:hover .tg-df-card-image {      transform: scale(1.05); /* Zoom in on hover */    }    .tg-df-card-discount-badge {      position: absolute;      top: 12px;      left: 12px;      background: #dc2626; /* Red */      color: #ffffff;      padding: 6px 8px;      font-size: 11px;      font-weight: 500;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      z-index: 10;    }        .tg-df-card-merchant-pill {      display: block;      padding: 0;      font-size: 11px;      font-weight: 600;      text-transform: uppercase;      letter-spacing: 0.5px;      border-radius: 0;      color: var(--tg-df-text-muted);      margin-bottom: 8px;      white-space: nowrap;      overflow: hidden;      text-overflow: ellipsis;    }    .tg-df-card-body {      padding: 16px;      display: flex;      flex-direction: column;      flex-grow: 1;      min-width: 0;    }    .tg-df-card-badges {      display: flex;      flex-wrap: wrap;      gap: 6px;      margin-bottom: 8px;    }    .tg-df-tag {      display: inline-flex;      align-items: center;      padding: 4px 6px;      font-size: 11px;      font-weight: 700;      text-transform: uppercase;      border-radius: 4px;      gap: 4px;    }    .tg-df-tag-prime {      background-color: #00A8E1;      color: #fff;    }    .tg-df-tag-coupons {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-coupons:hover {      background-color: #e2e8f0;    }        .tg-df-tag-outline {      background-color: #f1f5f9;      color: #334155;      border: 1px solid #cbd5e1;      cursor: pointer;      transition: background-color 0.2s;    }    .tg-df-tag-outline:hover {      background-color: #e2e8f0;    }        @keyframes tg-df-spin {      0% { transform: rotate(0deg); }      100% { transform: rotate(360deg); }    }    .tg-df-coupon-spinner {      border: 2px solid #e2e8f0;      border-top: 2px solid #3b82f6;      border-radius: 50%;      width: 14px;      height: 14px;      animation: tg-df-spin 1s linear infinite;      margin: 4px 8px;      display: inline-block;    }        /* Vouchers Modal */    .tg-df-modal-backdrop {      position: fixed;      top: 0; left: 0; right: 0; bottom: 0;      background: rgba(0,0,0,0.5);      z-index: 10000;      display: flex;      align-items: center;      justify-content: center;      opacity: 0;      pointer-events: none;      transition: opacity 0.3s;    }    .tg-df-modal-backdrop.active {      opacity: 1;      pointer-events: auto;    }    .tg-df-modal {      background: #fff;      border-radius: 12px;      width: 90%;      max-width: 400px;      max-height: 80vh;      display: flex;      flex-direction: column;      box-shadow: 0 10px 40px rgba(0,0,0,0.2);      transform: translateY(20px);      transition: transform 0.3s;    }    .tg-df-modal-backdrop.active .tg-df-modal {      transform: translateY(0);    }    .tg-df-modal-header {      padding: 16px;      border-bottom: 1px solid #e2e8f0;      display: flex;      align-items: center;      justify-content: space-between;    }    .tg-df-modal-title {      font-size: 16px;      font-weight: 600;      margin: 0;    }    .tg-df-modal-close {      background: none;      border: none;      cursor: pointer;      padding: 4px;      color: #64748b;    }    .tg-df-modal-body {      padding: 16px;      overflow-y: auto;    }    .tg-df-voucher-item {      padding: 12px;      border: 1px dashed #cbd5e1;      border-radius: 8px;      margin-bottom: 10px;      background: #f8fafc;      display: flex;      align-items: center;      gap: 12px;      text-decoration: none;      color: inherit;      transition: background-color 0.2s, border-color 0.2s;    }    .tg-df-voucher-item:hover {      background: #f1f5f9;      border-color: #94a3b8;    }    .tg-df-voucher-item:last-child {      margin-bottom: 0;    }    .tg-df-voucher-logo {      width: 48px;      height: 48px;      object-fit: contain;      border-radius: 4px;      background: #fff;      border: 1px solid #e2e8f0;      flex-shrink: 0;    }    .tg-df-voucher-content {      flex: 1;      min-width: 0;    }    .tg-df-voucher-title {      font-size: 14px;      font-weight: 600;      margin: 0 0 4px 0;      line-height: 1.3;      color: #0f172a;    }    .tg-df-voucher-expiry {      font-size: 12px;      color: #64748b;      display: flex;      align-items: center;      gap: 4px;      margin-top: 6px;    }    .tg-df-voucher-code {      display: inline-flex;      align-items: center;      background: #f1f5f9;      border: 1px dashed #cbd5e1;      padding: 6px 10px;      font-family: monospace;      font-weight: 700;      font-size: 14px;      color: #0f172a;      border-radius: 4px;      margin-top: 8px;      cursor: pointer;      transition: all 0.2s ease;    }    .tg-df-voucher-code:hover {      background: #e2e8f0;      border-color: #94a3b8;    }    .tg-df-voucher-code.copied {      background: #ecfdf5;      border-color: #10b981;      color: #10b981;    }    .tg-df-voucher-cta {      display: inline-block;      margin-top: 8px;      font-size: 13px;      font-weight: 600;      color: #2563eb;      text-decoration: none;    }    .tg-df-card-title {      font-size: 15px;      font-weight: 400;      line-height: 1.4;      margin: 0 0 12px 0;      color: var(--tg-df-text);      display: -webkit-box;      -webkit-line-clamp: 2;      -webkit-box-orient: vertical;      overflow: hidden;    }    .tg-df-card-footer {      margin-top: auto;      display: flex;      flex-direction: column;      width: 100%;    }    .tg-df-card-price-group {      display: flex;      flex-direction: row;      align-items: center;      gap: 8px;      margin-bottom: 12px;    }    .tg-df-card-price {      font-size: 16px;      font-weight: 700;      color: #dc2626; /* Red price */      line-height: 1;    }        .tg-df-card-msrp {      font-size: 13px;      color: var(--tg-df-text-muted);      text-decoration: line-through;    }    .tg-df-container .tg-df-card-cta {      display: flex;      align-items: center;      justify-content: center;      width: 100%;      box-sizing: border-box;      background-color: #1f69ff;      color: #ffffff;      font-size: 12px;      font-weight: 700;      text-transform: uppercase;      letter-spacing: 0.5px;      padding: 12px 16px;      border-radius: 0;      border: none;      cursor: pointer;      transition: background-color 0.2s ease;    }    .tg-df-card:hover .tg-df-card-cta,    .tg-df-card-cta:hover {      background-color: #1555cc;    }    .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad {      background-color: #3c8d0d;    }    .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad,    .tg-df-card-cta.tg-df-cta-savings-squad:hover {      background-color: #2b6509;    }    /*       5. State & Skeleton Styles    */    .tg-df-message {      grid-column: 1 / -1;      text-align: center;      padding: 48px 24px;      color: var(--tg-df-text-muted);      font-size: 16px;      background: var(--tg-df-bg);      border: 1px solid var(--tg-df-border);      border-radius: 8px;    }    @keyframes tg-df-shimmer {      0% { background-position: -200% 0; }      100% { background-position: 200% 0; }    }    .tg-df-skeleton {      background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%);      background-size: 200% 100%;      animation: tg-df-shimmer 1.5s infinite;      border-radius: 4px;    }    .tg-df-skeleton-img {      width: 100%;      height: 100%;      position: absolute;      top: 0; left: 0;    }        .tg-df-skeleton-text {      height: 16px;      margin-bottom: 8px;      width: 100%;    }    .tg-df-skeleton-text.short { width: 40%; }    .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; }    /* Editor Floating Bar & Elements */    .tg-df-editor-bar {      position: sticky;      top: 0;      z-index: 1000;      background: #111827;      color: #fff;      padding: 12px 16px;      border-radius: 8px;      margin-bottom: 16px;      display: flex;      align-items: center;      justify-content: space-between;      box-shadow: 0 4px 12px rgba(0,0,0,0.15);    }    .tg-df-editor-bar-text {      font-weight: 600;      font-size: 14px;    }    .tg-df-editor-copy-btn {      background: #10b981;      color: #fff;      padding: 6px 16px;      border: none;      border-radius: 4px;      font-weight: 600;      cursor: pointer;      display: flex;      align-items: center;      font-size: 13px;    }    .tg-df-editor-copy-btn:hover { background: #059669; }        .tg-df-deal-checkbox {      position: absolute;      top: 12px;      right: 12px;      z-index: 10;      width: 20px;      height: 20px;      cursor: pointer;      pointer-events: auto;    }    /*       6. Mobile List View (Stacks into a cleaner horizontal row/list)    */    @container tg-df (max-width: 599px) {      .tg-df-controls {        padding: 0 16px;      }            .tg-df-top-bar {        width: 100%;      }            .tg-df-settings-dropdown {        position: fixed;        top: auto;        bottom: 0;        left: 0;        right: 0;        width: 100%;        border-radius: 20px 20px 0 0;        padding: 24px;        box-shadow: 0 -8px 32px rgba(0,0,0,0.15);        z-index: 1000;        border: none;        border-top: 1px solid var(--tg-df-border);      }            .tg-df-settings-dropdown-backdrop.active {        background: rgba(0,0,0,0.4);      }            .tg-df-search-wrapper {        box-shadow: 0 0 16px rgba(0,0,0,0.08);      }            .tg-df-filters {        width: calc(100% + 32px);        margin: 0 -16px;        padding: 0 16px 4px 16px;        display: flex;        justify-content: flex-start;        gap: 8px;        flex-wrap: nowrap;        overflow-x: auto;        -webkit-overflow-scrolling: touch;        scrollbar-width: none;      }      .tg-df-filters::after {        content: "";        display: block;        flex: 0 0 8px;      }      .tg-df-filters::-webkit-scrollbar {        display: none;      }            .tg-df-sort-wrapper {        flex: 0 0 max(42%, 130px);        min-width: 0;      }      .tg-df-sort-wrapper.tg-df-price-range-wrapper {        flex: 0 0 auto;        min-width: max-content;      }            .tg-df-sort-select, .tg-df-filter-select {        width: 100%;        text-align: left;        padding: 10px 24px 10px 32px;        background-position: right 8px center;        text-overflow: ellipsis;        white-space: nowrap;        overflow: hidden;      }      .tg-df-sort-icon {        left: 10px;      }      .tg-df-grid:not(.layout-grid):not(.layout-row),      .tg-df-grid.layout-row {        grid-template-columns: 1fr;        gap: 16px;      }            .tg-df-grid.tg-df-grid-auto {        padding-top: 24px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card,      .tg-df-grid.layout-row .tg-df-card {        flex-direction: row;        align-items: stretch;        height: auto;        box-shadow: none; /* simple line on mobile if preferred, or keep */        border-bottom: 1px solid var(--tg-df-border);      }      .tg-df-grid.tg-df-grid-auto .tg-df-card:hover,      .tg-df-grid.layout-row .tg-df-card:hover {        box-shadow: none;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box,      .tg-df-grid.layout-row .tg-df-card-image-box {        width: 120px;        min-width: 120px;        aspect-ratio: 3/4;        border-right: none;        padding: 12px;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-body,      .tg-df-grid.layout-row .tg-df-card-body {        padding: 12px;        justify-content: space-between;      }      .tg-df-grid.tg-df-grid-auto .tg-df-card-title,      .tg-df-grid.layout-row .tg-df-card-title {        font-size: 14px;        margin-bottom: 12px;        -webkit-line-clamp: 3;      }      /* Single column mobile grid override */      .tg-df-grid.layout-grid {        grid-template-columns: 1fr;        gap: 16px;      }      .tg-df-grid.layout-grid .tg-df-card-image-box {        padding: 12px;      }      .tg-df-grid.layout-grid .tg-df-card-body {        padding: 10px;      }      .tg-df-grid.layout-grid .tg-df-card-title {        font-size: 13px;        -webkit-line-clamp: 3;        margin-bottom: 8px;      }      .tg-df-grid.layout-grid .tg-df-card-price {        font-size: 14px;      }            .tg-df-card-footer {        flex-direction: column;        align-items: stretch;        gap: 0;        width: 100%;        min-width: 0;      }      .tg-df-card-merchant-pill {        margin-bottom: 4px;      }      .tg-df-card-price-group {        flex: 1 1 auto;        margin-bottom: 8px;      }      .tg-df-card-price {        font-size: 16px;      }      .tg-df-card-msrp {        display: block;       }      .tg-df-grid.layout-row .tg-df-card-cta,      .tg-df-container .tg-df-card-cta {        width: 100%;        max-width: none;        min-width: 0;        box-sizing: border-box;        padding: 8px 16px;        font-size: 12px;        flex: 0 0 auto;        text-align: center;        white-space: normal;        line-height: 1.2;      }    }    .tg-df-container.is-carousel {      min-height: 760px;      background-color: #E7F0FF;      padding: 0 0 24px 0;      border-radius: 24px;    }    .tg-df-container.is-carousel.hide-header-details {      min-height: 480px;    }    /*       7. Carousel View Mode    */    .tg-df-container .tg-df-carousel-host {      /* Layout is now handled by container wrapper */    }    .tg-df-container .tg-df-carousel-eyebrow {      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      padding: 24px 16px 0 16px;      display: none;    }    .tg-df-container .tg-df-carousel-query-title {      color: #011535;      font-size: 28px;      font-weight: 600;      padding: 0 16px 24px 16px;      line-height: 1.2;      display: none;    }    .tg-df-container .tg-df-carousel-blue-box {      background-color: transparent;      border-radius: 0;      padding: 24px 24px 0 24px;      margin: 0;      color: #1F69FF;          position: relative;      overflow: hidden;    }    .tg-df-container .tg-df-carousel-bg-circle-1 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-2 {      display: none;    }    .tg-df-container .tg-df-carousel-bg-circle-3 {      display: none;    }    .tg-df-container .tg-df-carousel-box-content {      position: relative;      z-index: 10;    }    .tg-df-container .tg-df-carousel-box-eyebrow {      background-color: transparent;      color: #1F69FF;      font-weight: 700;      font-size: 14px;      text-transform: uppercase;      letter-spacing: 1px;      display: inline-block;      padding: 0;      border-radius: 0;    }    .tg-df-container .tg-df-carousel-box-title {      font-size: 28px;      font-weight: 600;      line-height: 1.2;      margin-top: 8px;      color: #1e293b;    }    .tg-df-container .tg-df-countdown-wrapper {      position: absolute;      top: 0;      right: 0;      display: flex;      flex-direction: column;      align-items: flex-end;      gap: 12px;      transform: scale(0.67);      transform-origin: top right;    }    .tg-df-container .tg-df-countdown-title {      font-size: 16px;      text-align: center;      width: 100%;      font-weight: 600;      color: #011535;      margin: 0;    }    .tg-df-container .tg-df-countdown-blocks {      display: flex;      gap: 16px;    }    .tg-df-container .tg-df-countdown-item {      display: flex;      flex-direction: column;      align-items: center;      gap: 4px;    }    .tg-df-container .tg-df-countdown-box {      width: 59px;      height: 59px;      background: #03FE9E;      border-radius: 15px;      display: flex;      align-items: center;      justify-content: center;    }    .tg-df-container .tg-df-countdown-num {      font-family: 'Inter', sans-serif;      font-weight: 700;      font-size: 20px;      line-height: normal;      color: #011535;    }    .tg-df-container .tg-df-countdown-label {      font-family: 'Inter', sans-serif;      font-weight: 500;      font-size: 16px;      line-height: normal;      color: #1e293b;      text-transform: uppercase;    }    .tg-df-container .tg-df-carousel-box-subtitle {      font-size: 16px;      margin-top: 8px;      font-weight: 300;      color: #1e293b;      line-height: 24px;    }    .tg-df-container .tg-df-carousel-roundels-wrapper {      position: relative;      margin-top: 24px;      margin-left: -24px;      margin-right: -24px;    }    .tg-df-container .tg-df-carousel-roundels {      display: flex;      gap: 16px;      overflow-x: auto;            scrollbar-width: none;      padding-top: 12px;      padding-bottom: 24px;      padding-left: 24px;      padding-right: 24px;      margin-left: 0;      margin-right: 0;    }    .tg-df-container .tg-df-carousel-scroll-right {      position: absolute;      right: 8px;      top: 50%;      transform: translateY(-50%);      height: 36px;      width: 36px;      display: flex;      align-items: center;      justify-content: center;      border-radius: 50%;      background-color: #ffffff;      border: 1px solid #e2e8f0;      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);      color: #1F69FF;      cursor: pointer;      transition: all 0.2s;      margin-top: -4px;      z-index: 20;    }    .tg-df-container .tg-df-carousel-scroll-right:hover {      background-color: #f8fafc;      border-color: #cbd5e1;    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right {      right: 0;      background-color: rgba(255, 255, 255, 0.4);      border: none;      box-shadow: none;      backdrop-filter: blur(4px);      -webkit-backdrop-filter: blur(4px);    }    .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover {      background-color: rgba(255, 255, 255, 0.6);      border: none;    }    .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar {      display: none;    }    .tg-df-container .tg-df-carousel-roundels::after {      content: "";      flex: 0 0 32px;    }    .tg-df-container .tg-df-roundel {      display: flex;      flex-direction: column;      align-items: center;      gap: 8px;      cursor: pointer;      min-width: 120px;      flex-shrink: 0;    }    .tg-df-container .tg-df-roundel-img-box {      width: 120px;      height: 120px;      border-radius: 50%;      background: white;      display: flex;      align-items: center;      justify-content: center;      overflow: hidden;      box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08);      transition: box-shadow 0.2s;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box {      box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF;    }    .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img {      transform: scale(1.08);    }    .tg-df-container .tg-df-roundel-img-box img {      width: 100%;      height: 100%;      object-fit: contain;      padding: 10px;      box-sizing: border-box;      transition: transform 0.3s ease;    }    .tg-df-container .tg-df-roundel-label {      font-size: 13px;      font-weight: 400;      color: #1e293b;      text-align: center;      transition: font-weight 0.2s;    }    .tg-df-container .tg-df-roundel.active .tg-df-roundel-label {      font-weight: 700;    }    .tg-df-container .tg-df-carousel-filters-label {      font-size: 16px;      font-weight: 400;      color: #1e293b;      white-space: nowrap;      margin-right: 4px;    }    .tg-df-container .tg-df-carousel-filters-wrap {      display: flex;      align-items: center;      flex-wrap: nowrap;      gap: 8px;      margin-top: 8px;      overflow-x: auto;      scrollbar-width: none;      -webkit-overflow-scrolling: touch;      padding-bottom: 8px;      margin-left: -24px;      margin-right: -24px;      padding-left: 24px;      padding-right: 24px;    }    .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar {      display: none;    }        .tg-df-container .tg-df-carousel-filter-btn img,    .tg-df-container .tg-df-carousel-filter-btn picture {      height: 20px;      width: 20px;      object-fit: contain;      object-position: center;      display: inline-flex;      align-items: center;      justify-content: center;      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn picture img {      margin-right: 0;      height: 100%;      width: 100%;    }    .tg-df-container .tg-df-carousel-filter-btn img.active-img,    .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) {      display: none;    }    .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img,    .tg-df-container .tg-df-carousel-filter-btn.active img.active-img,    .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img),    .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) {      display: inline-flex;    }    .tg-df-container .tg-df-carousel-filter-btn {      background: #ffffff;      border: 2px solid #1e293b;      color: #1e293b;      border-radius: 24px;      padding: 6px 16px;      font-size: 14px;      font-weight: 600;      cursor: pointer;      transition: all 0.2s;      flex-shrink: 0;      white-space: nowrap;      display: inline-flex;      align-items: center;      justify-content: center;      min-height: 36px;      box-sizing: border-box;    }    .tg-df-container .tg-df-carousel-filter-btn svg {      margin-right: 6px;    }    .tg-df-container .tg-df-carousel-filter-btn:hover {      background: #1e293b;      color: white;      border-color: #1e293b;    }    .tg-df-container .tg-df-carousel-filter-btn.active {      background: #1e293b;      color: white;      border-color: #1e293b;    }        .tg-df-grid.carousel-compact {      display: flex;      flex-wrap: nowrap;      overflow-x: auto;      gap: 16px;      padding: 16px 24px;      align-items: stretch;      scrollbar-width: none;    }    .tg-df-grid.carousel-compact::after {      content: "";      flex: 0 0 32px;    }    .tg-df-grid-wrapper {      position: relative;    }    .tg-df-grid.carousel-compact::-webkit-scrollbar {      display: none;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card {      flex: 0 0 auto;      width: 100px;      border-radius: 15px;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      border: 2px solid #1e293b;      background: white;      color: #1e293b;      display: flex;      flex-direction: column;      justify-content: center;      align-items: center;      font-weight: 600;      font-size: 14px;      cursor: pointer;      padding: 16px;      text-align: center;      transition: all 0.2s;    }    .tg-df-grid.carousel-compact .tg-df-load-more-card:hover {      background: #1e293b;      color: white;    }    .tg-df-grid.carousel-compact .tg-df-card {      flex: 0 0 auto;      width: 200px;      min-height: auto;      height: auto;      display: flex;      flex-direction: column;      border-radius: 15px;      border: none;      box-shadow: 0 0 16px rgba(0,0,0,0.08);      overflow: visible;    }    .tg-df-grid.carousel-compact .tg-df-card-image-box {      padding: 12px;      background-color: transparent;      border-radius: 15px 15px 0 0;      height: 130px;    }    .tg-df-grid.carousel-compact .tg-df-card-image {      mix-blend-mode: normal;    }    .tg-df-grid.carousel-compact .tg-df-card-discount-badge {      border-radius: 0;      top: 0px;      left: 0px;      padding: 4px 8px;      font-size: 11px;    }    .tg-df-grid.carousel-compact .tg-df-card-body {      padding: 8px 12px 12px 12px;    }    .tg-df-grid.carousel-compact .tg-df-card-title {      font-size: 14px;      font-weight: 400;      -webkit-line-clamp: 2;      margin-bottom: 8px;      color: #011535;    }    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title,    .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title {      -webkit-line-clamp: 3;    }    .tg-df-grid.carousel-compact .tg-df-card-cta {      border-radius: 5px;      padding: 8px 10px;      margin-top: 4px;      background-color: #1F69FF;    }    .tg-df-grid.carousel-compact .tg-df-card-price-group {      margin-bottom: 2px;    }    .tg-df-grid.carousel-compact .tg-df-card-merchant-pill {      margin-bottom: 2px;    }    @container tg-df (max-width: 599px) {      .tg-df-container .tg-df-carousel-blue-box-title {        font-size: 24px;      }      .tg-df-container .tg-df-countdown-title {        display: none;      }      .tg-df-container .tg-df-countdown-wrapper {        position: absolute;        top: 0;        right: 0;        align-items: flex-end;        transform: scale(0.45);        transform-origin: top right;      }      .tg-df-container .tg-df-roundel {        min-width: 88px;      }      .tg-df-container .tg-df-roundel-img-box {        width: 88px;        height: 88px;      }    }    /* REPLICA BLOCK STYLES */    .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; }    .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; }        .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; }    .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; }    .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; }    .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; }    .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; }    .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; }    .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; }    .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; }    .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; }    .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; }    .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; }    .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; }    .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; }        @container tg-df (min-width: 600px) {      .tg-df-mobile-only { display: none !important; }    }    @container tg-df (max-width: 599px) {      .tg-df-desktop-only { display: none !important; }      .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; }      .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; }    }    .tg-df-grid.savings-squad-cards .tg-df-card-title {      -webkit-line-clamp: unset !important;      display: block !important;      overflow: visible !important;    }    @container tg-df (max-width: 500px) {      .tg-df-container .hawk-deal-widget-wrap { display: block; }      .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; }      .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; }      .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; }      .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; }    }        .tg-df-container .hawk-affiliate-link-deal-button {       align-items: center; background-color: #5aaf0b; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s;     }    .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #4a9109; text-decoration: none; }    .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; }    .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; }    .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; }    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title,    .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; }  \x3C/style>  \x3C!-- Widget Container --\x3E  \x3Cdiv class="tg-df-container" id="signal-deals-finder-root">    \x3C!-- Editor Floating Bar --\x3E    \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;">      \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;">        \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected        \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button>      \x3C/div>      \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button">        \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg>        Copy to CMS      \x3C/button>    \x3C/div>    \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;">      \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div>      \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div>            \x3Cdiv class="tg-df-carousel-blue-box">        \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div>        \x3Cdiv class="tg-df-carousel-box-content">          \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;">            \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div>            \x3Cdiv class="tg-df-countdown-blocks">              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div>              \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div>            \x3C/div>          \x3C/div>          \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div>          \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div>          \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div>                    \x3Cdiv class="tg-df-carousel-roundels-wrapper">          \x3Cdiv class="tg-df-carousel-roundels">            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>            \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div>          \x3C/div>          \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-carousel-filters-wrap">                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime">              \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>            \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 10% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 15% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>            Min 25% off\x3C/button>                      \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50">              \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>            Under $50\x3C/button>\n        \x3C/div>\n      \x3C/div>\n    \x3C/div>          \x3C!-- Search & Filter Controls --\x3E    \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;">      \x3Cdiv class="tg-df-top-bar">        \x3Cdiv class="tg-df-search-wrapper">          \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands...">          \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search">              \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">                \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>              \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div>        \x3C/div>                \x3Cdiv class="tg-df-settings-wrapper">          \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle">            \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">                \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>            \x3C/svg>          \x3C/button>          \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div>          \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel">            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Search Region\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-region-select">                \x3Coption value="auto">🌍 Auto-detect\x3C/option>                \x3Coption value="US">🇺🇸 United States (US)\x3C/option>                \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option>                \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option>                \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option>                \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option>                \x3Coption value="FR">🇫🇷 France (FR)\x3C/option>                \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Retailer\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select">                \x3Coption value="">All Retailers\x3C/option>                \x3Coption value="Amazon">Amazon\x3C/option>                \x3Coption value="Walmart">Walmart\x3C/option>                \x3Coption value="Best Buy">Best Buy\x3C/option>                \x3Coption value="Target">Target\x3C/option>                \x3Coption value="John Lewis">John Lewis\x3C/option>                \x3Coption value="Currys">Currys\x3C/option>                \x3Coption value="Argos">Argos\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select">                \x3Coption value="">All Offers\x3C/option>                \x3Coption value="amazon_prime">Amazon Prime\x3C/option>                \x3Coption value="recommended_promo">Recommended Promo\x3C/option>                \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">Result Count\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-rows-select">                \x3Coption value="3">3 Items\x3C/option>                \x3Coption value="4">4 Items\x3C/option>                \x3Coption value="6">6 Items\x3C/option>                \x3Coption value="12" selected>12 Items\x3C/option>                \x3Coption value="24">24 Items\x3C/option>                \x3Coption value="48">48 Items\x3C/option>              \x3C/select>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-deal-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item tg-df-dl-row">              \x3Cdiv>                \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span>                \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span>              \x3C/div>              \x3Clabel class="tg-df-toggle">                \x3Cinput type="checkbox" id="tg-df-editor-mode">                \x3Cspan class="tg-df-slider">\x3C/span>              \x3C/label>            \x3C/div>            \x3Cdiv class="tg-df-setting-item">              \x3Clabel class="tg-df-setting-label">View Mode\x3C/label>              \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select">                \x3Coption value="auto">Auto Collection\x3C/option>                \x3Coption value="carousel">Carousel\x3C/option>                \x3Coption value="savings_squad">Savings Squad\x3C/option>                \x3Coption value="grid">Grid (Columns)\x3C/option>                \x3Coption value="row">Row (List)\x3C/option>              \x3C/select>            \x3C/div>          \x3C/div>        \x3C/div>      \x3C/div>      \x3Cdiv class="tg-df-filters">        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category">            \x3Coption value="all">All Categories\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/>          \x3C/svg>          \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0">            Any Brand          \x3C/div>          \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown">            \x3C!-- Populated via script --\x3E          \x3C/div>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>          \x3C/svg>          \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals">            \x3Coption value="date_desc">Newest First\x3C/option>            \x3Coption value="best_match">Sort by: Match\x3C/option>            \x3Coption value="price_asc">Price Low to High\x3C/option>            \x3Coption value="price_desc">Price High to Low\x3C/option>            \x3Coption value="discount_desc">Biggest Discount\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);">          \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">          \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span>          \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;">        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices">            \x3Coption value="all">All Prices\x3C/option>            \x3Coption value="under50">Under $50\x3C/option>            \x3Coption value="50_100">$50 - $100\x3C/option>            \x3Coption value="100_200">$100 - $200\x3C/option>            \x3Coption value="200_500">$200 - $500\x3C/option>            \x3Coption value="over500">Over $500\x3C/option>          \x3C/select>        \x3C/div>        \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper">          \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">            \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/>          \x3C/svg>          \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount">            \x3Coption value="all">Any discount\x3C/option>            \x3Coption value="5">Min 5%\x3C/option>            \x3Coption value="10">Min 10%\x3C/option>            \x3Coption value="15">Min 15%\x3C/option>            \x3Coption value="20">Min 20%\x3C/option>            \x3Coption value="25">Min 25%\x3C/option>            \x3Coption value="30">Min 30%\x3C/option>            \x3Coption value="40">Min 40%\x3C/option>            \x3Coption value="50">Min 50%\x3C/option>            \x3Coption value="60">Min 60%\x3C/option>            \x3Coption value="70">Min 70%\x3C/option>          \x3C/select>        \x3C/div>      \x3C/div>    \x3C/div>    \x3C!-- Deals Grid Wrapper --\x3E    \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper">      \x3Cdiv class="tg-df-grid" id="tg-df-grid">        \x3C!-- Content populated by JavaScript --\x3E      \x3C/div>    \x3C/div>        \x3C!-- Vouchers Modal --\x3E    \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal">      \x3Cdiv class="tg-df-modal">        \x3Cdiv class="tg-df-modal-header">          \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3>          \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close">            \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">              \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line>              \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line>            \x3C/svg>          \x3C/button>        \x3C/div>        \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content">          \x3C!-- Vouchers injected here --\x3E        \x3C/div>      \x3C/div>    \x3C/div>  \x3C/div>`;      if (!template) {        template = document.createElement('template');        template.innerHTML = rawTemplate;      }      let shadowRoot = null;      if (hostContainer && template) {        hostContainer.setAttribute('data-initialized', 'true');        shadowRoot = hostContainer.attachShadow({ mode: 'open' });        shadowRoot.appendChild(template.content.cloneNode(true));      }      class DealsFinderWidget {        constructor(config) {          this.rootNode = config.rootNode || document;          this.hostContainer = config.hostContainer || null;          this.rootId = config.rootId || 'signal-deals-finder-root';          this.root = this.rootNode.querySelector('#' + this.rootId);          if (!this.root) return;          this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2);          this.grid = this.root.querySelector('#tg-df-grid');          this.tagsContainer = this.root.querySelector('#tg-df-tags-container');          this.categoryFilter = this.root.querySelector('#tg-df-category-filter');          this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper');          this.searchInput = this.root.querySelector('.tg-df-search-input');          this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete');          this.sortSelect = this.root.querySelector('.tg-df-sort-select');          this.searchBtn = this.root.querySelector('.tg-df-search-btn');                    this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle');          this.settingsPanel = this.root.querySelector('#tg-df-settings-panel');          this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop');          this.regionSelect = this.root.querySelector('#tg-df-region-select');          this.retailerSelect = this.root.querySelector('#tg-df-retailer-select');          this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select');          this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select');          this.rowsSelect = this.root.querySelector('#tg-df-rows-select');          this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode');          this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode');          this.priceFilter = this.root.querySelector('#tg-df-price-filter');          this.discountFilter = this.root.querySelector('#tg-df-discount-filter');                    this.editorBar = this.root.querySelector('#tg-df-editor-bar');          this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count');          this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy');          this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear');                    this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php';          this.deals = [];          this.displayLimit = 12;          this.airedaleArticles = null;          this.airedaleTags = [];          this.airedaleTagCounts = {};          this.activeDealTag = null;          this.selectedBrands = [];          this.currentQuery = '';          this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false;          this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null;          this.selectedDeals = new Map();                    this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper');          this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger');          this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown');                    this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper');          this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min');          this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max');          this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper');          this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper');          this.initResizeObserver();          this.init();            if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); }        }        getViewMode() {          if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) {            return this.viewModeOverride;          }          return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');        }        applyLayoutMode() {          if (!this.grid) return;          const mode = this.getViewMode();          this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2');                    const carouselHost = this.root.querySelector('#tg-df-carousel-host');          const controlsDiv = this.root.querySelector('#tg-df-controls');          if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') {             if (mode === 'carousel') this.grid.classList.add('carousel-compact');             if (carouselHost) carouselHost.style.display = 'block';             if (controlsDiv) controlsDiv.style.display = 'none';             if (this.root.classList.contains('tg-df-container') && mode === 'carousel') {               this.root.classList.add('is-carousel');             } else if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          } else {             if (carouselHost) carouselHost.style.display = 'none';             if (controlsDiv) controlsDiv.style.display = 'flex';             if (this.root.classList.contains('tg-df-container')) {               this.root.classList.remove('is-carousel');             }          }          if (mode === 'grid') {            this.grid.classList.add('layout-grid');          } else if (mode === 'row') {            this.grid.classList.add('layout-row');          } else if (mode === 'savings_squad') {            this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards');          } else if (mode !== 'carousel') {            this.grid.classList.add('tg-df-grid-auto');          }                    const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper');          if (settingsWrapper) {            settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block';          }          if (this.customPriceWrapper) {             this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none';          }          if (this.legacyPriceWrapper) {             this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }          if (this.discountFilterWrapper) {             this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex';          }        }        initResizeObserver() {          try {            if (window.parent === window) return;          } catch (e) {            // cross origin frame check threw          }          const emitHeight = () => {            try {              const height = document.documentElement.scrollHeight || document.body.scrollHeight;              const msg = { type: 'embed-size', height: height };              if (window.parent && window.parent !== window) {                window.parent.postMessage(msg, '*');                window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*');              }            } catch (e) {}          };                    if (window.ResizeObserver) {            try {              const ro = new ResizeObserver(() => emitHeight());              ro.observe(document.body);              if (this.root) ro.observe(this.root);            } catch(e){ console.warn(e); }          }          window.addEventListener('resize', emitHeight);          setTimeout(emitHeight, 300);        }        initCountdown() {          this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper');                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          this.showCountdown = params.get('show_countdown') === 'true';          const showHeaderDetails = params.get('show_header_details') !== 'false';          const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow');          const title = this.root.querySelector('.tg-df-carousel-box-title');          const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle');          if (!showHeaderDetails) {            let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (containerElement) containerElement.classList.add('hide-header-details');            if (eyebrow) eyebrow.style.display = 'none';            if (title) title.style.display = 'none';            if (subtitle) subtitle.style.display = 'none';          }          if (!this.cdWrapper) return;          this.cdTitle = this.root.querySelector('#tg-df-countdown-title');          this.cdDays = this.root.querySelector('#tg-df-cd-days');          this.cdHrs = this.root.querySelector('#tg-df-cd-hrs');          this.cdMin = this.root.querySelector('#tg-df-cd-min');          this.cdSec = this.root.querySelector('#tg-df-cd-sec');          this.updateCountdown();          this.cdInterval = setInterval(() => this.updateCountdown(), 1000);        }        updateCountdown() {          if (!this.cdWrapper) return;          if (!this.showCountdown) {            this.cdWrapper.style.display = 'none';            return;          }          const area = this.getAreaCode();          let offset = '-04:00';          if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) {             offset = '+02:00';          } else if (['GB', 'IE', 'UK'].includes(area)) {             offset = '+01:00';          }          const startTime = new Date('2026-06-23T00:00:00' + offset).getTime();          const endTime = new Date('2026-06-26T00:00:00' + offset).getTime();          const now = Date.now();          let targetTime = 0;          if (now < startTime) {             targetTime = startTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in';             this.cdWrapper.style.display = 'flex';          } else if (now < endTime) {             targetTime = endTime;             if (this.cdTitle) this.cdTitle.textContent = 'Prime Day ends in';             this.cdWrapper.style.display = 'flex';          } else {             this.cdWrapper.style.display = 'none';             if (this.cdInterval) clearInterval(this.cdInterval);             return;          }          const diff = Math.max(0, targetTime - now);          const d = Math.floor(diff / (1000 * 60 * 60 * 24));          const h = Math.floor((diff / (1000 * 60 * 60)) % 24);          const m = Math.floor((diff / 1000 / 60) % 60);          const s = Math.floor((diff / 1000) % 60);          if (this.cdDays) this.cdDays.textContent = d;          if (this.cdHrs) this.cdHrs.textContent = h;          if (this.cdMin) this.cdMin.textContent = m;          if (this.cdSec) this.cdSec.textContent = s;        }        init() {          this.initCountdown();          try {            initAnalytics();          } catch (e) {            console.warn('Deals Widget Analytics Error:', e);          }                    this.bindEvents();                    let initialQuery = '';                    let searchSource = window.location.search;          if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) {            searchSource = this.hostContainer.getAttribute('data-widget-config');          } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) {            searchSource = window.__WIDGET_CONFIG__;          }          const params = new URLSearchParams(searchSource);          let initialViewMode = params.get('view_mode');          if (!params.has('search') && !params.has('q') && !params.has('query') && initialViewMode !== 'savings_squad') {             initialQuery = 'Everything';             if (this.discountFilter && !params.has('min_discount_ratio')) {               this.discountFilter.value = '5';             }          }                    if (this.regionSelect) {            this.regionSelect.value = params.get('region') || 'auto';            this.updatePriceDropdownCurrency();          }                    if (this.retailerSelect && params.has('retailer')) {            this.retailerSelect.value = params.get('retailer');          }                    if (params.has('brands')) {            const b = params.get('brands');            if (b) {              this.selectedBrands = b.split(',');            }          }                    if (this.offerTypeSelect && params.has('offer_type')) {            this.offerTypeSelect.value = params.get('offer_type');          }          if (params.has('bg_color')) {            const bg = params.get('bg_color');            if (bg === 'white') {              this.root.style.setProperty('background-color', '#ffffff', 'important');            } else if (bg === 'transparent') {              this.root.style.setProperty('background-color', 'transparent', 'important');            } else if (bg === 'light_blue') {              this.root.style.setProperty('background-color', '#E7F0FF', 'important');            }          } else {             this.root.style.removeProperty('background-color');          }                    if (params.has('view_mode')) {            if (this.viewModeSelect) {              this.viewModeSelect.value = params.get('view_mode');            } else {              this.viewModeOverride = params.get('view_mode');            }          }          if (this.rowsSelect && params.has('rows')) {            this.rowsSelect.value = params.get('rows');          }          if (params.has('price')) {            const priceVal = params.get('price');            if (this.priceFilter) {               // Try assigning it directly to select. If it's not present implicitly ignores               this.priceFilter.value = priceVal;            }            if (priceVal.includes('_')) {               const parts = priceVal.split('_');               if (this.customPriceMin && parts[0]) this.customPriceMin.value = parts[0];               if (this.customPriceMax && parts[1]) this.customPriceMax.value = parts[1];            }          }          if (this.discountFilter && params.has('min_discount_ratio')) {            // Need to convert back from ratio (e.g. 0.8) to select value (e.g. "20")            const ratioStr = params.get('min_discount_ratio');            const ratioFloat = parseFloat(ratioStr);            if (!isNaN(ratioFloat)) {               const percentage = Math.round((1 - ratioFloat) * 100);               this.discountFilter.value = percentage.toString();            }          }          if (this.sortSelect) {            this.sortSelect.value = params.get('sort') || 'discount_desc';          }          if (this.dealModeToggle && params.has('deal_mode')) {            this.dealModeToggle.checked = params.get('deal_mode') === 'true' || params.get('deal_mode') === '1';          }                    // Re-apply layout after params have updated control values          this.applyLayoutMode();                    if (params.get('search')) {            initialQuery = params.get('search');          } else if (params.get('q')) {            initialQuery = params.get('q');          } else if (params.get('query')) {            initialQuery = params.get('query');          }                    this.currentQuery = initialQuery;          if (this.searchInput) {            this.searchInput.value = this.currentQuery;          }                    if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {            this.fetchDeals(this.currentQuery);          } else {            this.render();          }        }        updatePriceDropdownCurrency() {          if (!this.priceFilter || !this.regionSelect) return;          const currencySymbols = {            'US': '$',            'GB': '£',            'CA': '$CA',            'AU': '$AU',            'DE': '€',            'FR': '€',            'IT': '€',          };          const area = this.getAreaCode();          const cur = currencySymbols[area || 'US'] || '$';                    const options = this.priceFilter.options;          for (let i = 0; i < options.length; i++) {            const opt = options[i];            if (opt.value === 'all') {              opt.innerText = 'All Prices';            } else if (opt.value === 'under50') {              opt.innerText = `Under ${cur}50`;            } else if (opt.value === '50_100') {              opt.innerText = `${cur}50 - ${cur}100`;            } else if (opt.value === '100_200') {              opt.innerText = `${cur}100 - ${cur}200`;            } else if (opt.value === '200_500') {              opt.innerText = `${cur}200 - ${cur}500`;            } else if (opt.value === 'over500') {              opt.innerText = `Over ${cur}500`;            }          }        }        populateBrandDropdown(values) {          if (!this.brandDropdown || !this.brandFilterWrapper) return;          this.brandFilterWrapper.style.display = 'flex'; // show the wrapper                    let html = '';          const allChecked = this.selectedBrands.length === 0 ? 'checked' : '';          const _div = '<' + '/div>';          const _span = '<' + '/span>';          html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`;                    values.forEach(v => {             if (!v.formatted_value || v.formatted_value === 'Any Brand') return;             const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : '';             html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`;          });                    this.brandDropdown.innerHTML = html;                    // Re-bind listeners          const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');          chks.forEach(chk => {            chk.addEventListener('change', (e) => {              const val = e.target.value;              if (val === '') {                this.selectedBrands = [];              } else {                if (e.target.checked) {                   if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val);                } else {                   this.selectedBrands = this.selectedBrands.filter(b => b !== val);                }              }                            if (this.selectedBrands.length === 0) {                 this.brandTrigger.innerText = 'Any Brand';              } else if (this.selectedBrands.length === 1) {                 this.brandTrigger.innerText = this.selectedBrands[0];              } else {                 this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;              }                            // Only call API if changed from UI interactions              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.updateURLParams();                 this.fetchDeals(this.currentQuery);              }            });          });                    // Update button text on load          if (this.selectedBrands.length === 0) {             this.brandTrigger.innerText = 'Any Brand';          } else if (this.selectedBrands.length === 1) {             this.brandTrigger.innerText = this.selectedBrands[0];          } else {             this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`;          }        }        updateURLParams() {          const url = new URL(window.location);          if (this.currentQuery && this.currentQuery !== 'Gaming laptops') {            url.searchParams.set('q', this.currentQuery);          } else {            url.searchParams.delete('q');            url.searchParams.delete('search');            url.searchParams.delete('query');          }                    if (this.regionSelect && this.regionSelect.value !== 'auto') {            url.searchParams.set('region', this.regionSelect.value);          } else {            url.searchParams.delete('region');          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.set('retailer', this.retailerSelect.value);          } else {            url.searchParams.delete('retailer');          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.set('brands', this.selectedBrands.join(','));          } else {            url.searchParams.delete('brands');          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.set('offer_type', this.offerTypeSelect.value);          } else {            url.searchParams.delete('offer_type');          }                    if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') {            url.searchParams.set('view_mode', this.viewModeSelect.value);          } else {            url.searchParams.delete('view_mode');          }                    if (this.rowsSelect && this.rowsSelect.value !== '12') {            url.searchParams.set('rows', this.rowsSelect.value);          } else {            url.searchParams.delete('rows');          }                    const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             url.searchParams.set('price', `${min}_${max}`);          } else if (this.priceFilter && this.priceFilter.value !== 'all') {            url.searchParams.set('price', this.priceFilter.value);          } else {            url.searchParams.delete('price');          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {               const ratio = (100 - v) / 100;               url.searchParams.set('min_discount_ratio', ratio.toString());            }          } else {            url.searchParams.delete('min_discount_ratio');          }                    if (this.sortSelect && this.sortSelect.value !== 'discount_desc') {            url.searchParams.set('sort', this.sortSelect.value);          } else {            url.searchParams.delete('sort');          }                    if (this.dealModeToggle && this.dealModeToggle.checked) {            url.searchParams.set('deal_mode', 'true');          } else {            url.searchParams.delete('deal_mode');          }                    window.history.replaceState({}, '', url);        }        bindEvents() {          const roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded');          roundels.forEach(r => {             r.addEventListener('click', () => {                const q = r.getAttribute('data-query');                const pr = r.getAttribute('data-pr');                if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: q,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = q;                const label = this.root.querySelector('#tg-df-carousel-title-label');                if (label) label.textContent = 'Best ' + q;                if (this.priceFilter) this.priceFilter.value = pr || 'all';                if (this.discountFilter) this.discountFilter.value = '5';                if (this.searchInput) this.searchInput.value = q;                                roundels.forEach(ro => ro.classList.remove('active'));                r.classList.add('active');                this.fetchDeals(this.currentQuery);             });          });          const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn');          discBtns.forEach(b => {             b.addEventListener('click', () => {                const d = b.getAttribute('data-d');                const pr = b.getAttribute('data-pr');                const ot = b.getAttribute('data-ot');                let label = b.innerText ? b.innerText.trim() : '';                let filterType = 'unknown';                let filterVal = 'unknown';                if (d !== null) { filterType = 'discount'; filterVal = d; }                else if (pr !== null) { filterType = 'price'; filterVal = pr; }                else if (ot !== null) { filterType = 'offertype'; filterVal = ot; }                if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label });                                if (d !== null) {                   if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d;                } else if (pr !== null) {                   if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr;                } else if (ot !== null) {                   if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot;                } else {                   if (this.discountFilter) this.discountFilter.value = '0';                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.offerTypeSelect) this.offerTypeSelect.value = 'all';                }                if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") {                   discBtns.forEach(ro => ro.classList.remove('active'));                   b.classList.add('active');                } else if (b.getAttribute("data-type") !== "custom") {                   // Only operate on hardcoded buttons (those without data-type)                   discBtns.forEach(ro => {                      if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active');                   });                                      let makeActive = true;                   if (d !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (pr !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   } else if (ot !== null) {                       if (b.classList.contains('active')) makeActive = false;                       discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') });                   }                                      if (makeActive) b.classList.add('active');                                      // Check if anything is active, if not activate "All"                   let anyActive = false;                   discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; });                   if (!anyActive) {                       discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); });                   }                }                                this.fetchDeals(this.currentQuery);             });          });          if (this.brandTrigger && this.brandDropdown) {            this.brandTrigger.addEventListener('click', () => {              this.brandDropdown.classList.toggle('active');            });            document.addEventListener('click', (e) => {              if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) {                this.brandDropdown.classList.remove('active');              }            });          }          let debounceTimer;          if(this.searchInput) {            this.searchInput.addEventListener('input', (e) => {              clearTimeout(debounceTimer);              const query = e.target.value.trim();              this.currentQuery = query;              if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) {                 const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5);                 if (matches.length > 0) {                    this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}<` + `/div>`).join('');                    this.autocompleteDropdown.classList.add('active');                 } else {                    this.autocompleteDropdown.classList.remove('active');                 }              } else if (this.autocompleteDropdown) {                 this.autocompleteDropdown.classList.remove('active');              }              debounceTimer = setTimeout(() => {                this.updateURLParams();                if (query.length > 2) {                  this.fetchDeals(query);                } else if (query.length === 0) {                  this.deals = [];                  this.render();                }              }, 400);            });            this.searchInput.addEventListener('keypress', (e) => {              if (e.key === 'Enter') {                if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');                clearTimeout(debounceTimer);                const query = e.target.value.trim();                this.currentQuery = query;                this.activeDealTag = null;                trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });                this.updateURLParams();                if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                   this.fetchDeals(query);                }              }            });          }          if (this.autocompleteDropdown) {             this.autocompleteDropdown.addEventListener('click', (e) => {                const item = e.target.closest('.tg-df-autocomplete-item');                if (item) {                   const tag = item.getAttribute('data-tag');                   this.currentQuery = tag;                   if (this.searchInput) this.searchInput.value = tag;                   this.activeDealTag = tag;                   this.autocompleteDropdown.classList.remove('active');                   this.updateURLParams();                   this.fetchDeals(tag);                }             });             document.addEventListener('click', (e) => {               if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) {                 this.autocompleteDropdown.classList.remove('active');               }             });          }          if (this.searchBtn) {            this.searchBtn.addEventListener('click', () => {              if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active');              clearTimeout(debounceTimer);              const query = this.searchInput.value.trim();              trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query });              this.activeDealTag = null;              this.currentQuery = query;              this.updateURLParams();              if (query.length > 2 || (this.getViewMode() === 'savings_squad')) {                 this.fetchDeals(query);              }            });          }          if(this.sortSelect) this.sortSelect.addEventListener('change', () => {            trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` });            this.updateURLParams();            if (this.deals.length > 0) {              this.sortData();              this.render();            }          });                    const priceFilter = this.root.querySelector('#tg-df-price-filter');          if (priceFilter) {            this.priceFilter = priceFilter;            this.priceFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          const updateCustomPrice = () => {             this.updateURLParams();             if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);             } else {                this.render();             }          };          if (this.customPriceMin) {             this.customPriceMin.addEventListener('change', updateCustomPrice);             this.customPriceMin.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          if (this.customPriceMax) {             this.customPriceMax.addEventListener('change', updateCustomPrice);             this.customPriceMax.addEventListener('keypress', (e) => {                if (e.key === 'Enter') updateCustomPrice();             });          }          const discountFilter = this.root.querySelector('#tg-df-discount-filter');          if (discountFilter) {            this.discountFilter = discountFilter;            this.discountFilter.addEventListener('change', () => {              trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }            });          }          if (this.categoryFilter) {            this.categoryFilter.addEventListener('change', (e) => {               const val = e.target.value === 'all' ? null : e.target.value;               this.activeDealTag = val;               this.fetchSavingsSquad();            });          }                    if (this.settingsToggle) {            this.settingsToggle.addEventListener('click', () => {              const o = this.settingsPanel.classList.toggle('active');              this.settingsBackdrop.classList.toggle('active');              if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' });            });          }                    if (this.settingsBackdrop) {            this.settingsBackdrop.addEventListener('click', () => {              this.settingsPanel.classList.remove('active');              this.settingsBackdrop.classList.remove('active');            });          }                    if (this.regionSelect) {            this.regionSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text });              this.updateURLParams();              this.updatePriceDropdownCurrency();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.retailerSelect) {            this.retailerSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.offerTypeSelect) {            this.offerTypeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text });              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.viewModeSelect) {            this._prevViewMode = this.viewModeSelect.value;            this.viewModeSelect.addEventListener('change', () => {              trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text });                            // Reset all active toggles and filters to prevent config carry-over              this.selectedBrands = [];              if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands';              if (this.brandDropdown) {                const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                chks.forEach(chk => { chk.checked = false; });              }              if (this.priceFilter) this.priceFilter.value = 'all';              if (this.customPriceMin) this.customPriceMin.value = '';              if (this.customPriceMax) this.customPriceMax.value = '';              if (this.sortSelect) this.sortSelect.value = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc';              if (this.discountFilter) this.discountFilter.value = '0';              if (this.retailerSelect) this.retailerSelect.value = '';              if (this.offerTypeSelect) this.offerTypeSelect.value = '';              if (this.rowsSelect) this.rowsSelect.value = '12';              if (this.categoryFilter) this.categoryFilter.value = 'all';              this.activeDealTag = null;              this.updateURLParams();              this.applyLayoutMode();                            if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') {                this.fetchDeals(this.currentQuery);              } else {                this.render();              }              this._prevViewMode = this.viewModeSelect.value;            });          }                    if (this.rowsSelect) {            this.rowsSelect.addEventListener('change', () => {              this.updateURLParams();              if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {                this.fetchDeals(this.currentQuery);              }            });          }                    if (this.dealModeToggle) {            this.dealModeToggle.addEventListener('change', () => {              this.updateURLParams();              this.render();            });          }          if (this.editorModeToggle) {             this.editorModeToggle.addEventListener('change', (e) => {                this.editorMode = e.target.checked;                this.render();                this.updateFloatingCopyBar();             });          }          if (this.editorCopyBtn) {             this.editorCopyBtn.addEventListener('click', () => {                this.copySelectedDealsToCMS();             });          }          if (this.editorClearBtn) {             this.editorClearBtn.addEventListener('click', () => {                this.selectedDeals.clear();                this.render();                this.updateFloatingCopyBar();             });          }          if (this.grid) {            this.grid.addEventListener('change', (e) => {               if (e.target.classList.contains('tg-df-deal-checkbox')) {                  const dealId = e.target.getAttribute('data-id');                  if (e.target.checked) {                     const dealObj = this.deals.find(d => d.id === dealId);                     if (dealObj) this.selectedDeals.set(dealId, dealObj);                  } else {                     this.selectedDeals.delete(dealId);                  }                  this.updateFloatingCopyBar();               }            });            this.grid.addEventListener('click', (e) => {              const dealCard = e.target.closest('[data-action="deal-click"]');              const similarCard = e.target.closest('[data-action="view-similar-click"]');              const cardLink = dealCard || similarCard;              if (cardLink) {                const productName = cardLink.getAttribute('data-product-name');                const merchantName = cardLink.getAttribute('data-merchant-name');                const productId = cardLink.getAttribute('data-analytics-id');                const price = parseFloat(cardLink.getAttribute('data-price')) || null;                const prevPriceStr = cardLink.getAttribute('data-previous-price');                const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null;                const originalLink = cardLink.getAttribute('data-original-link');                const rewrittenLink = cardLink.getAttribute('href');                const revenueId = cardLink.getAttribute('data-revenue-id');                const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0;                const inStock = cardLink.getAttribute('data-in-stock') === 'true';                const totalText = cardLink.getAttribute('data-total');                const totalDeals = parseInt(totalText, 10) || 0;                const productCategoryName = 'deals';                const trackingParams = {                  widgetId: this.widgetId,                  productCategoryName: productCategoryName,                  product: {                    modelId: cardLink.getAttribute('data-model-id') || null,                    matchId: cardLink.getAttribute('data-match-id') || null,                    brand: cardLink.getAttribute('data-model-brand') || null,                    parent: cardLink.getAttribute('data-model-parent') || null,                    name: productName,                    price: price,                    previousPrice: previousPrice,                    link: rewrittenLink,                    originalLink: originalLink,                    inStock: inStock                  },                  zeroBasedProductIndexOrNull: index,                  totalDealsOrProducts: totalDeals,                   merchant: {                    id: cardLink.getAttribute('data-merchant-id') || null,                    network: cardLink.getAttribute('data-merchant-network') || null,                    url: cardLink.getAttribute('data-merchant-url') || null,                    name: merchantName                  },                  revenueId: revenueId,                  widgetTypeName: this.widgetTypeName,                  isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$'))                };                if (dealCard) {                  trackDealClick(trackingParams);                } else {                  trackViewSimilarClick(trackingParams);                }              }              const couponsBtn = e.target.closest('[data-action="coupons-click"]');              if (couponsBtn) {                trackElementInteraction({                  id: 'product-card-show-coupons',                  name: 'Coupons',                  label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}`                });              }            });          }        }        get widgetTypeName() {          const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto');          switch(mode) {              case 'carousel': return 'Carousel';              case 'savings_squad': return 'Savings Squad';              case 'grid': return 'Grid';              case 'row': return 'Row';              default: return 'Auto Collection';          }        }        getAreaCode() {          if (this.regionSelect && this.regionSelect.value) {            if (this.regionSelect.value === 'auto') return null;            return this.regionSelect.value;          }          let area = null;          try {            const locale = window.navigator.language || window.navigator.userLanguage;            if (locale && locale.includes('-')) {              area = locale.split('-')[1].toUpperCase();            } else if (locale && locale.length === 2) {              if (locale.toUpperCase() === 'EN') { area = 'US'; }              else { area = locale.toUpperCase(); }            }          } catch (e) { /* Ignore */ }                    // Map to known valid options or fallback to US          const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT'];          if (area === 'UK') area = 'GB';          if (valid.includes(area)) {             return area;          }          return 'US';        }                async loadCarouselSpreadsheet() {          try {              const parseCSVRow = (str) => {                  let result = [], cur = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (inQuotes) {                          if (char === '"') {                              if (str[i + 1] === '"') { cur += '"'; i++; }                              else { inQuotes = false; }                          } else { cur += char; }                      } else {                          if (char === '"') { inQuotes = true; }                          else if (char === ',') { result.push(cur); cur = ''; }                          else { cur += char; }                      }                  }                  result.push(cur); return result;              };              const parseCSV = (str) => {                  const rows = []; let curRow = '', inQuotes = false;                  for (let i = 0; i < str.length; i++) {                      let char = str[i];                      if (char === '"') inQuotes = !inQuotes;                      if ((char === '\n' || char === '\r') && !inQuotes) {                          if (char === '\r' && str[i+1] === '\n') i++;                          if (curRow) rows.push(parseCSVRow(curRow));                          curRow = '';                      } else { curRow += char; }                  }                  if (curRow) rows.push(parseCSVRow(curRow));                  return rows;              };              const preloadedCSV = decodeURIComponent(escape(atob("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vcHJvZHVjdHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Byb2R1Y3RzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9wcm9kdWN0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/LCxBaXIgdGFncywsLCwsLCwsLCwsLCwsDQosLFBvcnRhYmxlIGNoYXJnZXJzL3dpcmVsZXNzIGNoYXJnZXJzLCwsLCwsLCwsLCwsLCwNCiwsIldhdGVyIGJvdHRsZXMgKHN0YW5sZXlzLCBPd2FsYSwgSHlkcm8gZmxhc2ssIFlldGkpIiwsLCwsLCwsLCwsLCwsDQosLEhhbmQgaGVsZCBmYW5zLCwsLCwsLCwsLCwsLCwNCiwsLCwsLCwsLCwsLCwsLCwNCmhvbWUgb2ZmaWNlLCxvZmZpY2UgY2hhaXJzLCwsLCwsLCwsLCwsLCwNCiwsc3RhbmRpbmcgZGVza3MsLCwsLCwsLCwsLCwsLA0KLCxtb25pdG9ycywsLCwsLCwsLCwsLCwsDQosLEtleWJvYXJkcywsLCwsLCwsLCwsLCwsDQosLGRvY2tpbmcgc3RhdGlvbiwsLCwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpHYW1pbmcsLENvbnNvbGVzLCwsLCwsLCwsLCwsLCwNCiwsQWNjZXNzb3JpZXMsLCwsLCwsLCwsLCwsLA0KLCxHYW1lcywsLCwsLCwsLCwsLCwsDQosLENvdWxkIGluY2x1ZGUgTGVnbz8sLCwsLCwsLCwsLCwsLA==")));              const text = preloadedCSV;              const parsed = parseCSV(text);                            const rowsByName = {};              let filterStart = -1;              parsed.forEach((rc, i) => {                 if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc;                 if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i;              });                            const cols = [];              if(rowsByName['Roundel text']) {                const headerRow = rowsByName['Roundel text'];                for(let col = 1; col < headerRow.length; col++) {                   let label = headerRow[col];                   if (!label) continue;                                      let q = rowsByName['Search Query'] && rowsByName['Search Query'][col] ? rowsByName['Search Query'][col] : '';                   let img = rowsByName['Roundel image'] && rowsByName['Roundel image'][col] ? rowsByName['Roundel image'][col] : '';                   let ds = rowsByName['Discount Amount'] && rowsByName['Discount Amount'][col] ? rowsByName['Discount Amount'][col] : '';                   let pr = rowsByName['Price Range'] && rowsByName['Price Range'][col] ? rowsByName['Price Range'][col] : '';                   let rt = rowsByName['Retailer'] && rowsByName['Retailer'][col] ? rowsByName['Retailer'][col] : '';                   let ot = rowsByName['Offer Type'] && rowsByName['Offer Type'][col] ? rowsByName['Offer Type'][col] : '';                                      let filters = [];                   if(filterStart > 0) {                     for(let r = filterStart + 1; r < parsed.length; r++) {                         if(!parsed[r] || parsed[r][0] === 'Notes' || parsed[r][0] === 'Categories to consider') break;                         let f = parsed[r][col];                         if(f) filters.push(f);                     }                   }                   cols.push({ label, img, q, ds, pr, rt, ot, filters });                }              }              this.carouselData = cols;              if (this.carouselData && this.carouselData.length > 0) {                 const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery);                 if (!isMatched) {                    const first = this.carouselData[0];                    this.currentQuery = first.q || first.label;                    if (this.priceFilter) this.priceFilter.value = 'all';                    if (this.customPriceMin) this.customPriceMin.value = '';                    if (this.customPriceMax) this.customPriceMax.value = '';                    let dPr = first.pr || 'all';                    if (typeof dPr === 'string' && dPr !== 'all') {                       let prLower = dPr.toLowerCase();                       if (prLower.includes('min') || prLower.includes('over')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else if (prLower.includes('max') || prLower.includes('under')) {                          let m = dPr.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       }                    }                    let dAm = '0';                    if(first.ds && typeof first.ds === 'string') {                       let m = first.ds.match(/(\d+)/);                       if(m) dAm = m[1];                    }                    if (this.discountFilter) this.discountFilter.value = dAm;                    if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || '';                    if (this.retailerSelect) this.retailerSelect.value = first.rt || '';                    this.selectedBrands = [];                    if (this.brandDropdown) {                        const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                        chks.forEach(chk => chk.checked = false);                    }                    if (this.searchInput) this.searchInput.value = this.currentQuery;                 }              }              this.renderCarouselUI();          } catch(e){ console.warn(e); }        }                renderCarouselUI() {           const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels');           if(!roundelWrapper || !this.carouselData) return;                      let html = '';           this.carouselData.forEach(r => {              const q = r.q || r.label;              const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : '';              const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`;              html += `                \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}">                  \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div>                  \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span>                \x3C/div>              `;           });           roundelWrapper.innerHTML = html;                      // Rebind clicks           const roundels = this.root.querySelectorAll('.tg-df-carousel-cat');           roundels.forEach(rNode => {             rNode.addEventListener('click', () => {                const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label'));                 if(!r) return;                                  if (typeof trackHawkEvent !== 'undefined') {                     trackHawkEvent({                         clickType: "CC",                         widgetId: this.widgetId,                         productCategoryName: "deals",                         zeroBasedProductIndexOrNull: null,                         totalDealsOrProducts: null,                         areaClicked: "Category Roundel",                         revenueId: this.revenueId,                         isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD',                         queryName: r.label,                         widgetTypeName: this.widgetTypeName                     });                 }                this.currentQuery = r.q || r.label;                const labelTitle = this.root.querySelector('#tg-df-carousel-title-label');                if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery;                if (this.priceFilter) this.priceFilter.value = 'all';                if (this.customPriceMin) this.customPriceMin.value = '';                if (this.customPriceMax) this.customPriceMax.value = '';                let dPr = r.pr || 'all';                if (typeof dPr === 'string' && dPr !== 'all') {                   let prLower = dPr.toLowerCase();                   if (prLower.includes('min') || prLower.includes('over')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMin) this.customPriceMin.value = m[1];                   } else if (prLower.includes('max') || prLower.includes('under')) {                      let m = dPr.match(/(\d+)/);                      if (m && this.customPriceMax) this.customPriceMax.value = m[1];                   }                }                                let discountAmount = '0';                if(r.ds && typeof r.ds === 'string') {                   let m = r.ds.match(/(\d+)/);                   if(m) discountAmount = m[1];                }                if (this.discountFilter) this.discountFilter.value = discountAmount;                if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                                // Clear brands                    this.selectedBrands = [];                    if (this.brandDropdown) {                    const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                    chks.forEach(chk => chk.checked = false);                }                                if (this.searchInput) this.searchInput.value = this.currentQuery;                                roundels.forEach(ro => ro.classList.remove('active'));                if (rNode) rNode.classList.add('active');                                this.renderCarouselFilters(r);                this.fetchDeals(this.currentQuery);             });           });                      // Auto-highlight active           const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery);           if(activeR) this.renderCarouselFilters(activeR);        }                renderCarouselFilters(r) {           const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap');           if(!filtersWrap) return;                      let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`;                      r.filters.forEach(f => {              let fL = f.toLowerCase();              let icon = '';              let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`;              if (fL === 'amazon deals' || fL === 'prime deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button>`;              } else if (fL === 'lightning deals') {                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button>`;              } else {                 if (fL.includes('lightning')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`;                 } else if (fL.includes('% off')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg>`;                 } else if (fL.includes('under') || fL.includes('min ')) {                    icon = `\x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg>`;                 }                 html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`;              }           });                      filtersWrap.innerHTML = html;                      const btns = filtersWrap.querySelectorAll('button');           btns.forEach(b => {             b.addEventListener('click', () => {                const type = b.getAttribute('data-type');                if (type === 'custom') {                   const v = b.getAttribute('data-v');                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v });                }                if (type === 'all') {                   if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' });                   // reset everything                   btns.forEach(btn => btn.classList.remove('active'));                   b.classList.add('active');                                      // Reset prices                   if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   if (this.brandDropdown) {                     const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                     chks.forEach(chk => chk.checked = false);                   }                } else {                   const v = b.getAttribute('data-v');                   const fL = v.toLowerCase();                                      let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos'];                   const getCategory = (s) => {                      if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer';                      if (s.includes('% off')) return 'discount';                      if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price';                      if (mapRet.includes(s)) return 'retailer';                      return 'brand';                   };                   const cat = getCategory(fL);                   const wasActive = b.classList.contains('active');                   if (cat !== 'brand') {                      btns.forEach(btn => {                          if (btn === b) return;                          if (btn.getAttribute('data-type') === 'all') return;                          const bV = btn.getAttribute('data-v');                          if (!bV) return;                          if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active');                      });                   }                   if (wasActive) b.classList.remove('active');                   else b.classList.add('active');                   let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active'));                   if (!anyActive) {                       btns[0].click();                       return;                   } else {                       btns[0].classList.remove('active');                   }                                      if (this.priceFilter) this.priceFilter.value = 'all';                   if (this.customPriceMin) this.customPriceMin.value = '';                   if (this.customPriceMax) this.customPriceMax.value = '';                   let dPr = r.pr || 'all';                   if (typeof dPr === 'string' && dPr !== 'all') {                      let prLower = dPr.toLowerCase();                      if (prLower.includes('min') || prLower.includes('over')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMin) this.customPriceMin.value = m[1];                      } else if (prLower.includes('max') || prLower.includes('under')) {                         let m = dPr.match(/(\d+)/);                         if (m && this.customPriceMax) this.customPriceMax.value = m[1];                      }                   }                                      let discountAmount = '0';                   if(r.ds && typeof r.ds === 'string') {                      let m = r.ds.match(/(\d+)/);                      if(m) discountAmount = m[1];                   }                   if (this.discountFilter) this.discountFilter.value = discountAmount;                   if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || '';                   if (this.retailerSelect) this.retailerSelect.value = r.rt || '';                   this.selectedBrands = [];                   btns.forEach(btn => {                       if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return;                       const vv = btn.getAttribute('data-v');                       const vl = vv.toLowerCase();                                              if (vl === 'lightning deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning';                       } else if (vl === 'amazon deals' || vl === 'prime deals') {                          if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime';                       } else if (vl.includes('% off')) {                          let m = vl.match(/(\d+)%/);                          if (m && this.discountFilter) this.discountFilter.value = m[1];                       } else if (vl.includes('under') || vl.includes('max')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMax) this.customPriceMax.value = m[1];                       } else if (vl.includes('min') || vl.includes('over')) {                          let m = vl.match(/(\d+)/);                          if (m && this.customPriceMin) this.customPriceMin.value = m[1];                       } else {                          let foundR = mapRet.find(x => x === vl);                          if (foundR) {                             let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl);                             if (this.retailerSelect) this.retailerSelect.value = realR;                          } else {                             this.selectedBrands.push(vv);                          }                       }                   });                                      if (this.brandDropdown) {                       const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk');                       chks.forEach(c => c.checked = this.selectedBrands.includes(c.value));                   }                                      if (r.pr && typeof r.pr === 'string') {                       let prL = r.pr.toLowerCase();                       if (prL.includes('under $')) {                           let m = prL.match(/under \$(\d+)/i);                           if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1];                       }                   }                }                                this.fetchDeals(this.currentQuery);             });           });                      // default to highlighting first           btns[0].classList.add('active');        }async fetchDeals(query, append = false) {          if (!append) {             this.showLoading();             this.deals = [];             this.displayLimit = (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          } else {             this.displayLimit += (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12;          }                    try {            if (this.getViewMode() === 'savings_squad') {               await this.fetchSavingsSquad(append);            } else {               if (this.isBroadQuery(query)) {                 await this.fetchAdviserDeals(query, append);               } else {                 await this.fetchHawkDeals(query, append);                 if (this.deals.length === 0) {                   await this.fetchAdviserDeals(query, append);                 }               }            }          } catch (error) {            console.warn("[Tom's Guide Widget] Fetch error:", error);            this.showError();          }        }        async fetchSavingsSquad() {          let topArticles = this.airedaleArticles;          if (!topArticles) {            const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`;            let res;            try {               res = await fetch(airedaleUrl);            } catch(e) {               try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; }            }            if (!res.ok) throw new Error('Airedale API Error');            const articles = await res.json();            topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);            this.airedaleArticles = topArticles;                        let tagCounts = {};            topArticles.forEach((a) => {              let articleTags = new Set();              if (a.articlecategory && Array.isArray(a.articlecategory)) {                 a.articlecategory.forEach((t) => articleTags.add(t));              }              articleTags.forEach(t => {                 tagCounts[t] = (tagCounts[t] || 0) + 1;              });            });                        this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]);            this.airedaleTagCounts = tagCounts;          }                    let targetArticles = topArticles;          if (this.activeDealTag) {             const encodedTag = encodeURIComponent(this.activeDealTag.toLowerCase().replace(/\s+/g, '-'));             const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`;             try {                const res = await fetch(url);                if (res.ok) {                   const articles = await res.json();                   targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []);                }             } catch(e) {                console.warn("Failed to fetch by activeDealTag", e);             }          }          let extractedDeals = [];          let dynamicBrandsCounts = {};                    targetArticles.forEach((article) => {             if (!article.articlepage) return;                          let pageData = [];             try {                pageData = JSON.parse(article.articlepage[0]);             } catch(e){ console.warn(e); }                          const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product');                          savingsSquad.forEach((block, idx) => {                const data = block.data || {};                const isFeatured = block.type === 'featured-product';                                const link = data.link || {};                const priceObj = data.price || {};                const image = data.image || {};                                if (data.brand) {                   data.brand = data.brand.replace(/^\d+\.\s*/, '').trim();                   dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1;                }                const externalUrl = isFeatured ? data.url : (link.href || null);                let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename);                let description = isFeatured ? (data.strapline || '') : (data.text || '');                                if (!isFeatured && !data.productName && data.text) {                   const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i'));                   if (brSplit.length > 1) {                     summaryTitle = brSplit[0].replace(/<[^>]+>/g, '').trim();                     description = brSplit.slice(1).join(' ').replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();                   } else {                     const match = data.text.match(/\x3Cstrong>(.*?)<\/strong>/);                     if (match) {                       summaryTitle = match[1].replace(/<[^>]+>/g, '').trim();                       if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1);                     }                   }                }                                let imageUrl = isFeatured ? image.mos : (image.src || null);                if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl;                                description = description.replace(/<br\s*\/?>/gi, ' ').replace(/<\/?(p|div)[^>]*>/gi, ' ').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim();                                let merchantName = data.retailer || '';                if (!merchantName && externalUrl) {                   try {                     merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0];                     merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1);                   }catch(e){ console.warn(e); }                }                if (!merchantName) merchantName = 'Retailer';                const q = (this.currentQuery || '').toLowerCase();                const activeTagLogic = (this.activeDealTag || '').toLowerCase();                if (q.length > 2 && q !== activeTagLogic) {                   const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase();                   if (!searchTarget.includes(q)) return;                }                let rawPrice = 0;                let rawMsrp = 0;                let currencyStr = '$';                if (isFeatured) {                   rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0);                   rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0;                   currencyStr = data.currency === 'GBP' ? '£' : '$';                } else {                   rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0;                   rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0;                   currencyStr = priceObj.currency === 'GBP' ? '£' : '$';                }                                let savingAmt = 0;                let savingLabel = '';                if (rawPrice > 0 && rawMsrp > rawPrice) {                   savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2));                   savingLabel = `Save ${currencyStr}${savingAmt}`;                }                                // Apply Brand filter                if (this.selectedBrands && this.selectedBrands.length > 0) {                   const itemBrand = (data.brand || '').toLowerCase();                   const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand);                   if (!hasMatch) return;                }                // Apply Price filter                let priceFilterVal = null;                const min = this.customPriceMin ? this.customPriceMin.value : '';                const max = this.customPriceMax ? this.customPriceMax.value : '';                if (min || max) {                   priceFilterVal = `${min}_${max}`;                } else if (this.priceFilter && this.priceFilter.value !== 'all') {                   priceFilterVal = this.priceFilter.value;                }                if (priceFilterVal && rawPrice > 0) {                   if (priceFilterVal === 'under50' && rawPrice >= 50) return;                   if (priceFilterVal === 'over50' && rawPrice <= 50) return;                   if (priceFilterVal === 'over30' && rawPrice <= 30) return;                   if (priceFilterVal === 'over500' && rawPrice <= 500) return;                   if (priceFilterVal.includes('_')) {                      const parts = priceFilterVal.split('_');                      const min = parseFloat(parts[0]);                      const max = parseFloat(parts[1]);                      if (!isNaN(min) && rawPrice < min) return;                      if (!isNaN(max) && rawPrice > max) return;                   }                }                // Apply Discount filter                if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {                   const requiredDiscount = parseInt(this.discountFilter.value);                   if (!isNaN(requiredDiscount) && requiredDiscount > 0) {                      if (!rawMsrp || rawMsrp <= rawPrice) return;                      const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100);                      if (ratio < requiredDiscount) return;                   }                }                                extractedDeals.push({                   id: `airedale-${article.id || Math.random()}-${idx}`,                   url: externalUrl,                   image: imageUrl,                   fallbackImage: imageUrl,                   title: summaryTitle,                   brand: data.brand || '',                   productName: data.productName || '',                   merchant: merchantName,                   rawPrice: rawPrice,                   rawMsrp: rawMsrp,                   price: rawPrice > 0 ? rawPrice.toString() : '',                   msrp: rawMsrp > 0 ? rawMsrp.toString() : '',                   currency: currencyStr,                   isCheckPrice: !rawPrice,                   savingLabel: savingLabel,                   savingType: rawMsrp > rawPrice ? 'amount' : 'none',                   isPrime: false,                   starRating: null,                   description: description,                   text: data.text || ''                });             });          });                    const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({              formatted_value: b,              count: dynamicBrandsCounts[b]          })).sort((a,b) => b.count - a.count);                    if (this.getViewMode() === 'savings_squad') {             this.populateBrandDropdown(airedaleBrandsList.slice(0, 15));             if (this.brandFilterWrapper) {                if (airedaleBrandsList.length === 0) {                    this.brandFilterWrapper.style.display = 'none';                } else {                    this.brandFilterWrapper.style.display = 'flex';                }             }          }                    this.deals = extractedDeals;          this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        isBroadQuery(query) {          const q = query.toLowerCase();          const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount'];          return intentModifiers.some(term => q.includes(term));        }        async fetchHawkDeals(query, append = false) {          const url = new URL(this.apiUrl);          url.searchParams.append('model_name', query);          const areaCode = this.getAreaCode();          if (areaCode) {            url.searchParams.append('area', areaCode);          }                    if (append && this.deals.length > 0) {            url.searchParams.append('offset', this.deals.length.toString());          }                    if (this.retailerSelect && this.retailerSelect.value) {            url.searchParams.append('filter_merchant_name', this.retailerSelect.value);          }                    if (this.selectedBrands && this.selectedBrands.length > 0) {            url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(','));          }                    let priceVal = null;          const min = this.customPriceMin ? this.customPriceMin.value : '';          const max = this.customPriceMax ? this.customPriceMax.value : '';          if (min || max) {             priceVal = `${min}_${max}`;          } else if (this.priceFilter && this.priceFilter.value !== 'all') {             priceVal = this.priceFilter.value;          }          if (priceVal) {            if (priceVal === 'under50') {              url.searchParams.append('filter_max_price', '50');            } else if (priceVal === 'over50') {              url.searchParams.append('filter_min_price', '50');            } else if (priceVal === 'over30') {              url.searchParams.append('filter_min_price', '30');            } else if (priceVal === 'over500') {              url.searchParams.append('filter_min_price', '500');            } else if (priceVal.includes('_')) {              const parts = priceVal.split('_');              if (parts[0]) url.searchParams.append('filter_min_price', parts[0]);              if (parts[1]) url.searchParams.append('filter_max_price', parts[1]);            }          }                    if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') {            const v = parseInt(this.discountFilter.value);            if (!isNaN(v) && v > 0) {              const ratio = (100 - v) / 100;              url.searchParams.append('min_discount_ratio', ratio.toString());            }          }                    if (this.offerTypeSelect && this.offerTypeSelect.value) {            url.searchParams.append('offer', this.offerTypeSelect.value);          }                    url.searchParams.append('filter_product_types', 'deals');                    if (this.rowsSelect && this.rowsSelect.value) {            url.searchParams.append('rows', this.rowsSelect.value);          } else {             url.searchParams.append('rows', '12'); // default          }          let response;          try {             response = await fetch(url.toString());          } catch(e) {             if (window.location.protocol === 'file:') {                console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock.");                await this.fetchAdviserDeals(query);                return;             }             console.warn("Hawk fetch failed", e);             this.deals = [];             this.render();             return;          }          if (!response.ok) {            throw new Error('Hawk API Response Error');          }          const rawData = await response.json();          // Safely locate data array from potentially wrapped response          let offers = [];          let modelInfoArray = [];                    let brandFilterData = null;          if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) {             brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand');          } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) {             brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand');          }          if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) {             this.populateBrandDropdown(brandFilterData.values);          } else {             if (this.brandFilterWrapper && this.selectedBrands.length === 0) {                this.brandFilterWrapper.style.display = 'none';             }          }                    if (rawData && rawData.widget && rawData.widget.data) {            if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers;            if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info);            }          } else if (rawData && rawData.data) {            if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers;            if (rawData.data.model_info && typeof rawData.data.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info);            }          } else {            if (Array.isArray(rawData)) offers = rawData;            else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers;            else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer;            else if (rawData && rawData.offers) offers = [].concat(rawData.offers);                        if (rawData && rawData.model_info && typeof rawData.model_info === 'object') {              modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info);            }          }          let modelDetails = {};          modelInfoArray.forEach(m => {            const mId = m.model_id || m.id;            if (mId) {              modelDetails[mId] = {                score: m.score != null ? parseFloat(m.score) : null,                brand: m.brand || null,                parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null              };            }          });          offers.forEach(item => {            let data = { ...item };            const mId = data.model_id;            if (mId && modelDetails[mId]) {              data.review_score = modelDetails[mId].score;              data.model_brand = modelDetails[mId].brand;              data.model_parent = modelDetails[mId].parent;            } else {              data.review_score = null;            }                        let itemOffers = [];            if (Array.isArray(item.offers)) itemOffers = item.offers;            else if (Array.isArray(item.offer)) itemOffers = item.offer;            else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers];            else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer];            if (itemOffers.length > 0) {              itemOffers.forEach(subItem => {                let subData = { ...item, ...subItem };                const subId = subData.model_id;                if (subId && modelDetails[subId]) {                  subData.review_score = modelDetails[subId].score;                  subData.model_brand = modelDetails[subId].brand;                  subData.model_parent = modelDetails[subId].parent;                } else if (data.review_score != null) {                  subData.review_score = data.review_score;                }                if (subData.merchant && typeof subData.merchant === 'object') {                  subData.merchant_name = subData.merchant.name;                }                this.deals.push(this.extractDealData(subData));              });              return;            }                        if (item.merchant && typeof item.merchant === 'object') {              data.merchant_name = item.merchant.name;            }                        this.deals.push(this.extractDealData(data));          });                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        async fetchAdviserDeals(query) {          // ======================================================================          // TODO: ADVISER API REPLACEMENT          // The code below simulates the Adviser API response using mock data.          // Once the real endpoint is ready, remove getAdviserMockData() and           // perform an actual fetch() request similar to fetchHawkDeals().          // Example:          // const area = this.getAreaCode();          // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`;          // if (this.priceFilter && this.priceFilter.value !== 'all') {          //   const val = this.priceFilter.value;          //   if (val === 'under50') apiUrl += '&filter_max_price=50';          //   else if (val === '50_100') apiUrl += '&filter_max_price=100';          //   else if (val === '100_200') apiUrl += '&filter_max_price=200';          //   else if (val === '200_500') apiUrl += '&filter_max_price=500';          // }          // const res = await fetch(apiUrl);          // const rawData = await res.json();          // ======================================================================          // Simulating network latency          await new Promise(resolve => setTimeout(resolve, 400));                    const rawData = this.getAdviserMockData();          let offers = [];                    if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) {            offers = rawData.data.Get.Deal;          }                    // Basic client-side filtering for the mock if we want it to react to the query          const q = query.toLowerCase();          const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null;                    offers.forEach(item => {            const dataObj = item;                        // Apply retailer filter            const itemRetailer = (dataObj.dataRetailer || '').toLowerCase();            if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) {              return;            }                        // Apply mock price filter            let price = dataObj.dataDiscountedPrice || 0;            if (typeof price === 'string') {              price = parseFloat(price.replace(/[^0-9.]/g, ''));            }            let priceVal = null;            const min = this.customPriceMin ? this.customPriceMin.value : '';            const max = this.customPriceMax ? this.customPriceMax.value : '';            if (min || max) {               priceVal = `${min}_${max}`;            } else if (this.priceFilter && this.priceFilter.value !== 'all') {               priceVal = this.priceFilter.value;            }            if (priceVal) {              if (priceVal === 'under50' && price >= 50) return;              if (priceVal === 'over50' && price <= 50) return;              if (priceVal === 'over30' && price <= 30) return;              if (priceVal === 'over500' && price <= 500) return;              if (priceVal.includes('_')) {                 const parts = priceVal.split('_');                 if (parts[0] && price < parseFloat(parts[0])) return;                 if (parts[1] && price > parseFloat(parts[1])) return;              }            }                        // Map Adviser schema to our widget's expected schema            const mappedData = {              url: dataObj.linkHREF || dataObj.dataLink || '#',              image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '',              title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal',              merchant: dataObj.dataRetailer || 'Retailer',              price: dataObj.dataDiscountedPrice || 0,              currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'),              msrp: dataObj.dataOriginalPrice || null            };                        const titleLow = mappedData.title.toLowerCase();            const merchLow = mappedData.merchant.toLowerCase();                        // Smarter mock filtering            let isMatch = false;            if (q === '' || this.isBroadQuery(q)) {              isMatch = true;            } else if (titleLow.includes(q) || merchLow.includes(q)) {              isMatch = true;            } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) {              isMatch = true;            } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) {              isMatch = true;            } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) {              isMatch = true;            } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) {              isMatch = true;            }                        if (isMatch) {               this.deals.push(this.extractDealData(mappedData));            }          });                    let rowLimit = 12;          if (this.rowsSelect && this.rowsSelect.value) {            rowLimit = parseInt(this.rowsSelect.value, 10) || 12;          }          // Intentionally omitting the slice here to allow "Load More" to work if the API returns more                    this.sortData();          this.render();          if (typeof trackDealsAppeared !== 'undefined') {             trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName);          }        }        getAdviserMockData() {          return {            "data": {              "Get": {                "Deal": [                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 300,                    "dataOriginalPrice": 399,                    "dataProduct": "Samsung Galaxy A36",                    "dataRetailer": "Samsung",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 14,                    "dataOriginalPrice": 24,                    "dataProduct": "Blink Mini",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 59,                    "dataOriginalPrice": 99,                    "dataProduct": "Ring Video Doorbell",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 10,                    "dataOriginalPrice": 599,                    "dataProduct": "MacBook Neo",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 749,                    "dataOriginalPrice": 849,                    "dataProduct": "65\\\" Fire TV Omni 4K QLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 71,                    "dataOriginalPrice": 160,                    "dataProduct": "Saucony Hurricane 24",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 649,                    "dataOriginalPrice": 749,                    "dataProduct": "Garmin Forerunner 970",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1049,                    "dataOriginalPrice": 1499,                    "dataProduct": "LG 48\\\" C4 4K OLED TV",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 1499,                    "dataOriginalPrice": 2199,                    "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor",                    "dataRetailer": "Amazon",                    "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png"                  },                  {                    "dataCurrency": "USD",                    "dataDiscountedPrice": 299,                    "dataOriginalPrice": 699,                    "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)",                    "dataRetailer": "Amazon",                    "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg"                  }                ]              }            }          };        }        decodeHTML(html) {          if (!html) return '';          const txt = document.createElement("textarea");          txt.innerHTML = String(html);          return txt.value;        }        extractDealData(item) {          const priceRawStr = String(item.price || item.current_price || '0');          const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0');          const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0;          const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0;          const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0';                    let originalImageUrl = item.image || item.image_url || item.product_image || '';          let imageUrl = originalImageUrl;          if ((!imageUrl || isCheckPrice) && item.model_image_url) {             imageUrl = item.model_image_url;             originalImageUrl = imageUrl;          } else if ((!imageUrl || isCheckPrice) && item.model_image) {             imageUrl = item.model_image;             originalImageUrl = imageUrl;          }                    if (imageUrl) {            imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3');          }                    let fallbackImage = '';          if (originalImageUrl && originalImageUrl !== imageUrl) {             fallbackImage = originalImageUrl;          } else if (item.model_image && item.model_image !== imageUrl) {             fallbackImage = item.model_image;          } else if (item.model_image_url && item.model_image_url !== imageUrl) {             fallbackImage = item.model_image_url;          }                    const rawCurrency = item.currency || item.currency_symbol || '$';                    let savingLabel = item.percentage_saving_label || '';          if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) {            const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100);            if (pct > 0) {              savingLabel = `${pct}% OFF`;            }          }                    const isPrime = item.shipping && item.shipping.prime === true;                    let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null;          let starRating = 0;          if (scoreRaw !== null) {            starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2;          }                    return {            id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(),            url: item.link || item.url || item.offer_link || '#',            image: imageUrl,            fallbackImage: fallbackImage,            title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product',            brand: item.brand || '',            productName: item.model_name || item.product_name || item.name || '',            merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer',            price: item.price !== undefined ? String(item.price) : '0.00',            currency: this.decodeHTML(rawCurrency),            msrp: item.was_price || item.msrp || item.original_price || null,            rawPrice: rawPrice,            rawMsrp: rawMsrp,            hasWasPrice: (item.was_price !== undefined && item.was_price !== null),            isCheckPrice: isCheckPrice,            savingLabel: savingLabel,            isPrime: isPrime,            starRating: starRating > 0 ? starRating : null,            modelId: item.model_id || '',            productKey: item.product_key || '',            merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '',            matchId: item.match_id || '',            merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '',            merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '',            modelBrand: item.model_brand || item.brand || '',            modelParent: item.model_parent || ''          };        }        sortData() {          const sortVal = this.sortSelect ? this.sortSelect.value : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_desc');          if (sortVal === 'price_asc') {            this.deals.sort((a, b) => a.rawPrice - b.rawPrice);          } else if (sortVal === 'price_desc') {            this.deals.sort((a, b) => b.rawPrice - a.rawPrice);          } else if (sortVal === 'discount_desc') {            this.deals.sort((a, b) => {              const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0;              const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0;              return bDiscount - aDiscount;            });          } else if (sortVal === 'date_desc') {             this.deals.sort((a, b) => {                let dateA = 0;                let dateB = 0;                if (a && a.modifiedDate) {                   const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate;                   dateA = new Date(valA).getTime();                   if (isNaN(dateA)) dateA = 0;                }                if (b && b.modifiedDate) {                   const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate;                   dateB = new Date(valB).getTime();                   if (isNaN(dateB)) dateB = 0;                }                return dateB - dateA;             });          }        }        getFilteredDeals() {          let filteredDeals = [...this.deals];                    if (this.dealModeToggle && this.dealModeToggle.checked) {            filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice));          }                    return filteredDeals;        }        showLoading() {          const _div = '<' + '/div>';          const skeletonCardHtml = `            \x3Cdiv class="tg-df-card">              \x3Cdiv class="tg-df-card-image-box">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div}              ${_div}              \x3Cdiv class="tg-df-card-body">                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div}                \x3Cdiv class="tg-df-card-footer mt-auto">                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div}                  \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div}                ${_div}              ${_div}            ${_div}`;          this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join('');        }        showError() {          const _div = '<' + '/div>';          this.grid.innerHTML = `\x3Cdiv class="tg-df-message">            An error occurred while finding deals. Please check your connection and try again.          ${_div}`;        }        escapeHTML(str) {          if (!str) return '';          return String(str).replace(/[&<>'"]/g, tag => ({              '&': '&', '<': '<', '>': '>', "'": ''', '"': '"'          }[tag] || tag));        }                bindCouponButtons() {          const btns = this.root.querySelectorAll('.tg-df-tag-coupons');          btns.forEach(btn => {            btn.addEventListener('click', (e) => {              e.preventDefault();              e.stopPropagation();              const merchant = btn.getAttribute('data-merchant');              this.openVouchersModal(merchant);            });          });                    const closeBtn = this.root.querySelector('#tg-df-vouchers-close');          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (closeBtn) {            closeBtn.onclick = () => this.closeVouchersModal();          }          if (backdrop) {            backdrop.onclick = (e) => {              if (e.target === backdrop) this.closeVouchersModal();            };          }        }                closeVouchersModal() {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          if (backdrop) backdrop.classList.remove('active');        }                async checkMerchantsCouponsBulk(merchants) {          if (!merchants || merchants.length === 0) return {};          const controller = new AbortController();          const timeoutId = setTimeout(() => controller.abort(), 4000);          try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchants.join(','));            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '120');            url.searchParams.append('origin', 'widgets-clientside');                        let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; }            clearTimeout(timeoutId);            if (!res.ok) return {};            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        const foundMerchants = new Set();            offers.forEach(o => {              let mName = o.merchant_name || o.merchant || o.retailer;              if (mName && typeof mName === 'object') mName = mName.name;              if (mName) foundMerchants.add(String(mName).toLowerCase());            });            const resultMap = {};            merchants.forEach(m => {              if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase());            });            return resultMap;          } catch (e) {            return {};          }        }                async openVouchersModal(merchantName) {          const backdrop = this.root.querySelector('#tg-df-vouchers-modal');          const title = this.root.querySelector('#tg-df-vouchers-title');          const content = this.root.querySelector('#tg-df-vouchers-content');                    if (!backdrop || !content) return;                    // HACK: Hide closing tags          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h4 = '<' + '/h4>';          const _svg = '<' + '/svg>';          const _circle = '<' + '/circle>';          const _polyline = '<' + '/polyline>';          const _rect = '<' + '/rect>';          const _path = '<' + '/path>';                    title.innerText = `${merchantName} Coupons & Deals`;          content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}                               \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`;          backdrop.classList.add('active');                    try {            const area = this.getAreaCode();            const url = new URL('https://search-api.fie.future.net.uk/widget.php');            url.searchParams.append('model_name', 'Everything');            url.searchParams.append('language', 'en-GB');            if (area) url.searchParams.append('area', area);            url.searchParams.append('combine_product_types', '1');            url.searchParams.append('filter_merchant_name', merchantName);            url.searchParams.append('all_filters', 'false');            url.searchParams.append('exclude_unlabelled', 'false');            url.searchParams.append('include_specs', 'false');            url.searchParams.append('sort', 'voucher');            url.searchParams.append('distinct_merchants', 'natural');            url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');            url.searchParams.append('rows', '50');            url.searchParams.append('origin', 'widgets-clientside');                        const res = await fetch(url.toString());            if (!res.ok) throw new Error('API Error');            const data = await res.json();                        let offers = [];            if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {              offers = data.widget.data.offers;            } else if (data && data.data && Array.isArray(data.data.offers)) {              offers = data.data.offers;            } else if (Array.isArray(data)) {              offers = data;            } else if (data && Array.isArray(data.offers)) {              offers = data.offers;            } else if (data && data.offers && Array.isArray(data.offers.offer)) {              offers = data.offers.offer;            } else if (data && Array.isArray(data.data)) {              offers = data.data;            }                        if (offers.length === 0) {              content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`;              return;            }                        content.innerHTML = offers.map(v => {              let offerObj = v;              if (v.offers && v.offers.offer) {                offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer;              } else if (v.offer) {                offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer;              }              let logoUrl = v.logo_url || offerObj.logo_url || '';              if (!logoUrl && v.merchant) {                if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || '';                else logoUrl = v.merchant.logo_url || '';              }                            const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer';              const endTime = offerObj.end_time || v.end_time || '';              const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#';                            let foundVoucherCode = '';              const findVoucherCode = (obj) => {                if (!obj || typeof obj !== 'object') return;                if (obj.type === 'voucher_code' && obj.display_value) {                  foundVoucherCode = obj.display_value;                  return;                }                if (Array.isArray(obj)) {                  for (const item of obj) {                    findVoucherCode(item);                    if (foundVoucherCode) return;                  }                } else {                  for (const k in obj) {                    if (Object.prototype.hasOwnProperty.call(obj, k)) {                      findVoucherCode(obj[k]);                      if (foundVoucherCode) return;                    }                  }                }              };              findVoucherCode(offerObj);              if (!foundVoucherCode) findVoucherCode(v);                            const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || '';              const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard">                \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span}                \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon">                  \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect}                  \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path}                ${_svg}              ${_span}` : '';                            const logoHtml = logoUrl                 ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />`                 : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`;                            let expiryHtml = '';              if (endTime) {                let dStr = endTime;                if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000;                const d = new Date(dStr);                if (!isNaN(d.getTime())) {                  const options = { year: 'numeric', month: 'short', day: 'numeric' };                  expiryHtml = `                    \x3Cdiv class="tg-df-voucher-expiry">                      \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                        \x3Ccircle cx="12" cy="12" r="10">${_circle}                        \x3Cpolyline points="12 6 12 12 16 14">${_polyline}                      ${_svg}                      Expires ${d.toLocaleDateString(undefined, options)}                    ${_div}`;                }              }              return `                \x3Ca href="${this.escapeHTML(linkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item">                  ${logoHtml}                  \x3Cdiv class="tg-df-voucher-content">                    \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4}                    ${codeHtml}                    ${expiryHtml}                  ${_div}                ${_a}              `;            }).join('');                        // Attach copy functionality            const copyBtns = content.querySelectorAll('[data-action="copy-code"]');            copyBtns.forEach(btn => {              btn.addEventListener('click', async (e) => {                e.preventDefault();                e.stopPropagation();                                const code = btn.getAttribute('data-code');                if (!code) return;                                try {                  const copyToClipboard = async (text) => {                     if (window.navigator.clipboard && window.isSecureContext) {                        try { await window.navigator.clipboard.writeText(text); return; } catch (e) {}                     }                     const textArea = document.createElement("textarea");                     textArea.value = text;                     textArea.style.position = "fixed";                     document.body.appendChild(textArea);                     textArea.focus();                     textArea.select();                     document.execCommand('copy');                     textArea.remove();                  };                  await copyToClipboard(code);                                    // Visual feedback                  btn.classList.add('copied');                  const textSpan = btn.querySelector('.tg-df-voucher-code-text');                  const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon');                                    const origText = textSpan.innerText;                  const origIcon = iconSvg.innerHTML;                                    textSpan.innerText = 'Copied!';                  iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`;                                    setTimeout(() => {                    if (btn) {                      btn.classList.remove('copied');                      if (textSpan) textSpan.innerText = origText;                      if (iconSvg) iconSvg.innerHTML = origIcon;                    }                  }, 2000);                                    trackElementInteraction({                    id: 'voucher-code-copy',                    name: 'Copy Voucher Code',                    label: `Copied ${code} for ${merchantName}`                  });                } catch (err) {                  console.warn('Failed to copy text: ', err);                }              });            });                                  } catch (e) {            console.warn(e);            content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`;          }        }        render() {          try {            if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) {              if (this.categoryFilterWrapper) {                 this.categoryFilterWrapper.style.display = 'flex';              }              if (this.categoryFilter) {                 const _option = '<' + '/option>';                 let optionsHtml = `\x3Coption value="all">All Categories${_option}`;                 this.airedaleTags.forEach(tag => {                    const isSelected = this.activeDealTag === tag ? 'selected' : '';                    optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)} (${this.airedaleTagCounts[tag] || 0})${_option}`;                 });                 this.categoryFilter.innerHTML = optionsHtml;                 this.categoryFilter.value = this.activeDealTag || 'all';              }            } else {               if (this.categoryFilterWrapper) {                  this.categoryFilterWrapper.style.display = 'none';               }            }            const displayDeals = this.getFilteredDeals();          // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection          const _div = '<' + '/div>';          const _span = '<' + '/span>';          const _a = '<' + '/a>';          const _h3 = '<' + '/h3>';          const _p = '<' + '/p>';          const _strong = '<' + '/strong>';          const _sup = '<' + '/sup>';          const _button = '<' + '/button>';          if (displayDeals.length === 0) {            if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) {              if (this.deals.length > 0) {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No deals match your selected filters.                ${_div}`;              } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length <= 2) {                 // Do not show "no exact matches" if query is empty for savings_squad                 this.grid.innerHTML = '';              } else {                 this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                  No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term.                ${_div}`;              }            } else {              this.grid.innerHTML = `\x3Cdiv class="tg-df-message">                Search product or category names to discover the best deals from across the web.              ${_div}`;            }            return;          }          let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => {            try {               const currencySym = this.escapeHTML(deal.currency);               const isoCurrencyCode = normalizeCurrency(currencySym);               const escapedPrice = this.escapeHTML(deal.price);               const escapedMsrp = this.escapeHTML(deal.msrp);               const areaCode = this.getAreaCode();                              const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null);               const originalLink = deal.url;               const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId);                        const productCategoryName = 'deals';            const dataAttr = `              data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}"              data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}"              data-product-name="${this.escapeHTML(deal.title)}"              data-merchant-name="${this.escapeHTML(deal.merchant)}"              data-price="${deal.rawPrice || ''}"              data-previous-price="${deal.rawMsrp || ''}"              data-original-link="${this.escapeHTML(originalLink)}"              data-revenue-id="${revenueId}"              data-index="${index}"              data-total="${displayDeals.length}"              data-in-stock="${deal.inStock !== false}"              data-currency="${this.escapeHTML(isoCurrencyCode)}"              data-model-id="${this.escapeHTML(deal.modelId || '')}"              data-product-key="${this.escapeHTML(deal.productKey || '')}"              data-merchant-id="${this.escapeHTML(deal.merchantId || '')}"            `;                        let priceGroupHtml = '';            let isSavingsSquadMode = this.getViewMode() === 'savings_squad';            let ctaText = 'View Deal';            let formattedPrice = '';            let msrpHtml = '';                        if (deal.isCheckPrice) {              ctaText = isSavingsSquadMode ? 'View Deal' : 'Check Price';              if (isSavingsSquadMode) {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                  ${_div}                `;              } else {                priceGroupHtml = `                  \x3Cdiv class="tg-df-card-merchant-wrapper">                    \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                  ${_div}                  \x3Cdiv class="tg-df-card-price-group">                    \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span}                  ${_div}                `;              }            } else {              // Format Price              formattedPrice = escapedPrice.includes(currencySym)                 ? escapedPrice                 : `${currencySym}${escapedPrice}`;                              // Format MSRP              msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice                ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}`                : '';                              priceGroupHtml = `                \x3Cdiv class="tg-df-card-merchant-wrapper">                  \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span}                ${_div}                \x3Cdiv class="tg-df-card-price-group">                  ${isSavingsSquadMode ? '' : `                  \x3Cspan class="tg-df-card-price">${formattedPrice}${_span}                  ${msrpHtml}                  `}                ${_div}              `;            }                        const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice              ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}`              : '';                          // HACK for CMS            const _button = '<' + '/button>';            const _svg = '<' + '/svg>';            const _path = '<' + '/path>';            const _rect = '<' + '/rect>';            const _circle = '<' + '/circle>';            const _polyline = '<' + '/polyline>';            const _line = '<' + '/line>';                        let badgesHtml = '';            const primeBadge = deal.isPrime ? `              \x3Cspan class="tg-df-tag tg-df-tag-prime">                \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">                  \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path}                ${_svg} Prime              ${_span}            ` : '';                        const couponsBadge = `              \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;">                \x3Cdiv class="tg-df-coupon-spinner">${_div}                \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;">                  \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">                    \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path}                    \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line}                  ${_svg} Coupons                ${_button}              ${_div}            `;                        // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state            badgesHtml = `              \x3Cdiv class="tg-df-card-badges">                ${primeBadge}                ${couponsBadge}              ${_div}            `;            const _linearGradient = '<' + '/linearGradient>';            const _polygon = '<' + '/polygon>';            const _stop = '<' + '/stop>';            const _defs = '<' + '/defs>';                        let starHtml = '';            if (deal.starRating) {              let rating = deal.starRating;                            if (rating > 0) {                const fullStars = Math.floor(rating);                const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0;                const emptyStars = Math.max(0, 5 - fullStars - halfStar);                const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button                const gray = '#cbd5e1';                                const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                const gradId = 'half_grad_' + Math.floor(Math.random()*1000000);                const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs}                  \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`;                                  const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`;                                let stars = [];                for (let i=0; i<fullStars; i++) stars.push(starSvgFull);                if (halfStar) stars.push(starSvgHalf);                for (let i=0; i<emptyStars; i++) stars.push(starSvgEmpty);                                starHtml = `\x3Cdiv class="tg-df-card-stars" style="display:flex;align-items:center;margin-bottom:8px;font-size:13px;font-weight:600;color:var(--tg-df-text-muted);">                  \x3Cspan style="margin-right:6px;">Tom's Guide:${_span}                  \x3Cdiv style="display:flex;gap:2px;">                    ${stars.join('')}                  ${_div}                ${_div}`;              }            }            let htmlOutput = '';            if (isSavingsSquadMode) {              htmlOutput += `              \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''}                \x3Cdiv class="hawk-deal-widget-wrap">                  \x3Cdiv class="hawk-deal-widget-image-container">                    \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}>                      \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                    ${_a}                  ${_div}                  \x3Cdiv class="hawk-deal-widget-text-cta-container">                    \x3Cdiv class="hawk-deal-widget-text-body-container">                      \x3Cdiv class="hawk-deal-widget-text-body-main">                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          ${deal.isCheckPrice ? `                            \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span}                          ` : `                            \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span}                          `}                        ${_a}                        ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span}                          ${_a}                        ` : ''}                        \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}>                          \x3Cspan class="hawk-deal-widget-title-retailer-price">                            ${!deal.isCheckPrice ? `                              \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span}                              \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span}                            ` : `                              \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span}                            `}                          ${_span}                        ${_a}                        ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_div}` : ''}                      ${_div}                    ${_div}                    \x3Cdiv class="hawk-deal-widget-footer">                      \x3Cdiv class="hawk-deal-widget-button-wrapper">                        \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper">                          \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}>                            \x3Cspan>${deal.isCheckPrice ? 'Check Price' : 'View Deal'}${_span}                          ${_a}                        ${_div}                      ${_div}                    ${_div}                  ${_div}                ${_div}              ${_div}              `;            }            htmlOutput += `              \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}">                ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''}                \x3Cdiv class="tg-df-card-image-box">                  ${discountBadgeHtml}                  \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;">                    \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}">                  ${_a}                ${_div}                \x3Cdiv class="tg-df-card-body">                  ${starHtml}                  ${badgesHtml}                  \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}">                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;">                      ${isSavingsSquadMode                         ? (deal.isCheckPrice                             ? (deal.title && deal.title.includes(':')                                 ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}`                                : this.escapeHTML(deal.title)                              )                             : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}`                          )                        : this.escapeHTML(deal.title)                      }                    ${_a}                  ${_h3}                  ${deal.description ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''}                  \x3Cdiv class="tg-df-card-footer">                    ${priceGroupHtml}                    \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a}                  ${_div}                ${_div}              ${_div}            `;                        return htmlOutput;            } catch (e) {               console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message);               return '';            }          }).join('');                    if (displayDeals.length > this.displayLimit || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) {            if (this.getViewMode() === 'carousel') {               dealsHtml += `                 \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more">                   \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg>                   Load More                 ${_button}               `;            } else {               dealsHtml += `                 \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;">                   \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer; display: flex; align-items: center;">Load More${_button}                 ${_div}               `;            }          }                    this.grid.innerHTML = dealsHtml;                    let gridWrapper = this.grid.parentElement;          if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) {             let existingChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right');             if (this.getViewMode() === 'carousel') {                 if (!existingChevron) {                     gridWrapper.insertAdjacentHTML('beforeend', '\n                 \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.previousElementSibling.scrollBy({left: 200, behavior: \'smooth\'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button>');                 }             } else {                 if (existingChevron) {                     existingChevron.remove();                 }             }          }                    const loadMoreBtn = this.grid.querySelector('.tg-df-load-more');          if (loadMoreBtn) {            loadMoreBtn.addEventListener('click', async () => {              if (typeof trackElementInteraction === 'function') {                trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' });              }              if (displayDeals.length <= this.displayLimit) {                 loadMoreBtn.innerHTML = `                  <svg class="tg-df-spinner" style="width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 8px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>                  Loading...                 `;                 loadMoreBtn.disabled = true;                 await this.fetchDeals(this.currentQuery, true);              } else {                 this.displayLimit += ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12);                 this.render();              }            });          }                      this.bindCouponButtons();            this.checkAndUpdateCoupons();                        // Allow hawklinks.js to discover and rewrite our widget links             // by appending the .article-body class and manually triggering processArticle.            let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container');            if (container && !container.classList.contains('article-body')) {               container.classList.add('article-body');            }            setTimeout(() => {               if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body');            if (!this.processArticleFired) {                  this.processArticleFired = true;                  document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } }));               }            }, 50);          } catch(e) {            console.warn("Widget render error", e);          }        }                async checkAndUpdateCoupons() {          const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper'));          if (wrappers.length === 0) return;                    const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))];          if (merchants.length === 0) return;          const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants);                    for (const merchant of merchants) {            const hasCoupons = !!couponResultsMap[merchant];            const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant);            merchantWrappers.forEach(wrapper => {              const spinner = wrapper.querySelector('.tg-df-coupon-spinner');              const btn = wrapper.querySelector('.tg-df-tag-coupons');                            if (spinner) spinner.style.display = 'none';                            if (hasCoupons && btn) {                btn.style.display = 'inline-flex';              } else if (!hasCoupons) {                wrapper.style.display = 'none';              }            });          }        }        updateFloatingCopyBar() {          if (!this.editorBar || !this.editorSelectedCount) return;          if (this.editorMode && this.selectedDeals.size > 0) {            this.editorBar.style.display = 'flex';            this.editorSelectedCount.innerText = this.selectedDeals.size;          } else {            this.editorBar.style.display = 'none';          }        }        async copySelectedDealsToCMS() {           function htmlToSlate(htmlString) {              if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }];              let doc;              if (typeof window !== 'undefined' && window.DOMParser) {                 doc = new DOMParser().parseFromString(htmlString, 'text/html');              } else {                 doc = document.implementation.createHTMLDocument('');                 doc.body.innerHTML = htmlString;              }                            function parseNode(node, marks = {}) {                  if (node.nodeType === 3) {                      const text = node.textContent;                      if (!text) return null;                      return { text: text, ...marks };                  }                  if (node.nodeType === 1) {                      const tagName = node.tagName.toLowerCase();                      if (tagName === 'br') {                          return { type: 'line-break', children: [{ text: '' }] };                      }                      if (tagName === 'p') {                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return { type: 'paragraph', children };                      }                      if (tagName === 'strong' || tagName === 'b') {                          const newMarks = { ...marks, bold: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'em' || tagName === 'i') {                          const newMarks = { ...marks, italic: true };                          return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean);                      }                      if (tagName === 'a') {                          const href = node.getAttribute('href') || '';                          let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                          if (children.length === 0) children.push({ text: "" });                          return {                              type: 'link',                              url: href,                              isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'),                              isSponsored: (node.getAttribute('rel') || '').includes('sponsored'),                              isOpenNewTab: node.getAttribute('target') === '_blank',                              isPreventDataRewrite: false,                              children: children                          };                      }                      return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean);                  }                  return null;              }                            let blocksArray = [];              let currentParagraphChildren = [];              function flushParagraph() {                  if (currentParagraphChildren.length > 0) {                      blocksArray.push({ type: 'paragraph', children: currentParagraphChildren });                      currentParagraphChildren = [];                  }              }              Array.from(doc.body.childNodes).forEach(node => {                  const parsed = parseNode(node, {});                  const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []);                  parsedItems.forEach(item => {                      if (item.type === 'paragraph') {                          flushParagraph();                          blocksArray.push(item);                      } else {                          currentParagraphChildren.push(item);                      }                  });              });              flushParagraph();              if (blocksArray.length === 0) {                  blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }];              }              return blocksArray;           }           const blocks = [];                      this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + '<' + '/span> Copying...';           for (const deal of Array.from(this.selectedDeals.values())) {              const url = deal.url;              const merchant = deal.merchant;              const title = deal.title;              const image = deal.image;              const currentPrice = deal.currency + deal.rawPrice;              const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : '';                            let couponsChildren = [];              try {                  const area = this.getAreaCode();                  const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php');                  apiUrl.searchParams.append('model_name', 'Everything');                  apiUrl.searchParams.append('language', 'en-GB');                  apiUrl.searchParams.append('area', area);                  apiUrl.searchParams.append('combine_product_types', '1');                  apiUrl.searchParams.append('filter_merchant_name', merchant);                  apiUrl.searchParams.append('all_filters', 'false');                  apiUrl.searchParams.append('exclude_unlabelled', 'false');                  apiUrl.searchParams.append('include_specs', 'false');                  apiUrl.searchParams.append('sort', 'voucher');                  apiUrl.searchParams.append('distinct_merchants', 'natural');                  apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter');                  apiUrl.searchParams.append('rows', '3');                  apiUrl.searchParams.append('origin', 'widgets-clientside');                                    let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; }                  if (res.ok) {                      const data = await res.json();                      let offers = [];                      if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) {                        offers = data.widget.data.offers;                      } else if (data && data.data && Array.isArray(data.data.offers)) {                        offers = data.data.offers;                      }                                            if (offers.length > 0) {                          couponsChildren.push({ text: "Also check out these coupons: ", bold: true });                          offers.slice(0, 3).forEach((offer, idx) => {                              const actualOffer = offer.offer || offer;                              const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon';                              const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#';                              couponsChildren.push({ type: "line-break", children: [{ text: "" }] });                              couponsChildren.push({ text: "🎟️ " });                              couponsChildren.push({                                  type: "link",                                  url: linkUrl,                                  isNoFollow: true,                                  isSponsored: false,                                  isOpenNewTab: true,                                  isPreventDataRewrite: false,                                  children: [{ text: offerName, bold: true }]                              });                          });                      }                  }              } catch (err) {                  console.warn('Failed to fetch coupons for', merchant, err);              }              let descriptionValue = [];              if (deal.text) {                 descriptionValue = htmlToSlate(deal.text);              } else {                 const dealDescriptions = [                   `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`,                   `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`,                   `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`,                   `If you're in the market for the ${title}, ${merchant} has just the deal for you.`,                   `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`,                   `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.`                 ];                 const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)];                 descriptionValue = [                    { type: "paragraph", children: [{ text: randomDescription }] }                 ];              }                            if (couponsChildren.length > 0) {                 let lastBlock = descriptionValue[descriptionValue.length - 1];                 if (lastBlock && lastBlock.type === 'paragraph') {                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children.push({ text: "Also check out these coupons: ", bold: true });                     lastBlock.children.push({ type: "line-break", children: [{ text: "" }] });                     lastBlock.children = lastBlock.children.concat(couponsChildren);                 } else {                     descriptionValue.push({                         type: "paragraph",                         children: [                             { type: "line-break", children: [{ text: "" }] },                             { type: "line-break", children: [{ text: "" }] },                             { text: "Also check out these coupons: ", bold: true },                             { type: "line-break", children: [{ text: "" }] },                             ...couponsChildren                         ]                     });                 }              }              function normalizeCurrencyToISO(symbol) {                const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' };                return map[symbol] || symbol;              }              const isoCurrency = normalizeCurrencyToISO(deal.currency);              blocks.push({                 id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(),                 blockTypeName: "deal",                 excludeFrom: [],                 collapsible: false,                 props: {                    description: {                       value: descriptionValue,                       touched: false,                       validationMessage: ""                    },                    image: {                       value: {                          credit: [{ type: "paragraph", children: [{ text: merchant }] }],                          dateCreated: Date.now(),                          dateModified: Date.now(),                          distribution: [],                          fileSize: 0,                          height: 1000,                          id: deal.id,                          imageRights: "",                          src: image,                          name: title + ".jpg",                          tags: [],                          width: 1000                       },                       touched: false,                       validationMessage: ""                    },                    showDealButton: { value: true, touched: false, validationMessage: "" },                    isPreferredPartner: { value: false, touched: false, validationMessage: "" },                    linkHref: { value: url, touched: false, validationMessage: "" },                    linkLabel: { value: "", touched: false, validationMessage: "" },                    linkIsNoFollow: { value: true, touched: false, validationMessage: "" },                    linkIsSponsored: { value: false, touched: false, validationMessage: "" },                    linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" },                    customPromoFlags: { value: [], touched: false, validationMessage: "" },                    showStarDeal: { value: false, touched: false, validationMessage: "" },                    savingType: { value: "none", touched: false, validationMessage: "" },                    starDealPromoFlag: { value: "", touched: false, validationMessage: "" },                    showEditorsChoice: { value: false, touched: false, validationMessage: "" },                    editorsChoiceTitle: { value: "", touched: false, validationMessage: "" },                    hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" },                    hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" },                    hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" },                    lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" },                    hawkModel: { touched: false, validationMessage: "" },                    productId: { value: "", touched: false, validationMessage: "" },                    voucherId: { value: "", touched: false, validationMessage: "" },                    brand: { value: deal.brand || merchant, touched: false, validationMessage: "" },                    productName: { value: title, touched: false, validationMessage: "" },                    label: { value: "", touched: false, validationMessage: "" },                    retailer: { value: merchant, touched: false, validationMessage: "" },                    priceCheckError: false                 },                 failedFetchError: ""              });           }           const payload = {              type: "articleBuilderPages",              data: blocks           };           const jsonStr = JSON.stringify(payload);                      if (navigator.clipboard && navigator.clipboard.writeText) {              navigator.clipboard.writeText(jsonStr).then(() => {                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              }).catch(err => {                 console.warn('Failed to copy text: ', err);                 alert('Failed to copy deals to clipboard. See console.');              });           } else {              // Fallback              const textArea = document.createElement("textarea");              textArea.value = jsonStr;              document.body.appendChild(textArea);              textArea.focus();              textArea.select();              try {                 document.execCommand('copy');                 this.editorCopyBtn.innerHTML = 'Copied!';                 setTimeout(() => {                    this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2"><' + '/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"><' + '/path><' + '/svg> Copy to CMS';                 }, 2000);              } catch (err) {                 console.warn('Fallback: Oops, unable to copy', err);                 alert('Fallback: Failed to copy deals to clipboard.');              }              document.body.removeChild(textArea);           }        }      }      // Initialize the Widget      if (document.readyState === 'loading') {        document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }));      } else {        new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer });      }    })();  </script></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I thought I could be the life of the party with the Soundboks Mix — but its shortcomings make me want to buy another party speaker instead ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Small, portable Bluetooth speakers can sound really good, but let's be honest: if you're soundtracking a large gathering or party, a small one isn't going to cut it. You need a proper party speaker for that, one that gets incredibly loud and makes your guests feel like they're at a concert. How about one that hits a staggering 121dB, then?</p><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/soundboks-mix-review">Soundboks Mix</a> is perhaps the loudest speaker I've tested yet. It's loud, but even at the highest volume, music doesn't sound too distorted, and the treble and mids remain surprisingly clear. Bass is nothing short of Earth-shaking, and the room-filling sound leaves you drooling. But...</p><div class="product"><a data-dimension112="dc054b3e-61a7-4a29-b6c3-43def3c6fc70" data-action="Deal Block" data-label="Mix" data-dimension48="Mix" data-dimension25="$799" href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:661px;"><p class="vanilla-image-block" style="padding-top:141.45%;"><img id="sypUHzwyPGjW57FYtGkA6k" name="Soundboks-Mix-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/sypUHzwyPGjW57FYtGkA6k.jpg" mos="" align="middle" fullscreen="" width="661" height="935" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Soundboks Mix is the brand’s portable party speaker, and it packs impressive audio capabilities. Ground-shaking bass, crisp treble, and refined mids make listening to music and soundtracking a party a treat. It’s IP65-rated so it’s protected against the elements, and it comes with a user-friendly companion app for customizing the equalizer. However, there aren’t as many connection ports on it as its closest rival, and it uses Bluetooth 4.2.<a class="view-deal button" href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow" data-dimension112="dc054b3e-61a7-4a29-b6c3-43def3c6fc70" data-action="Deal Block" data-label="Mix" data-dimension48="Mix" data-dimension25="$799">View Deal</a></p></div><p>Yeah, there's always a "but." It's part and parcel of testing and reviewing products, because few (if any) are perfect. The Mix is no different. Even though I love its sound profile, its drawbacks have left me feeling shortchanged — and when $799 is on the line, you want bang for your buck. There is <em>one </em>speaker out there that's great value for money, and sadly, it ain't the Mix.</p><h2 id="shake-the-ground">Shake the ground</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Syt6GuUeRig4JF24bZU8cG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/Syt6GuUeRig4JF24bZU8cG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of the Soundboks Mix's biggest selling points is that it's get ridiculously loud. At a whopping 121dB, the Mix sounds loud enough to vibrate a glass off a desk if you're indoors, or shake the ground when you're outdoors. Metaphorically speaking, of course — I didn't <em>actually </em>manage to drop a glass off a countertop.</p><p>All that's to say is that you can certainly be the life of the party with the Mix. Whether you're head-banging to bass-heavy rock or dancing to electronic dance music, the Mix has you covered. Two 72W Class D amplifiers powered by a  10-inch woofer and a 1.2-inch dome tweeter cover a frequency response range of 40Hz to 20KHz — typical for a speaker of its demeanour.</p><p>Regardless of the tune I listened to, I was impressed. Lana Del Rey's vocals in ‘<a href="https://open.qobuz.com/album/gcsfc9y5o0or4" target="_blank" rel="nofollow">First Light</a>’ sat front and centre and sounded remarkably clear. The warmth, texture and emotional nuance were reproduced accurately, while subtle details in her delivery found themselves in the spotlight without sounding too clinical. The treble in rock tunes, like ‘<a href="https://open.qobuz.com/track/361933224" target="_blank" rel="nofollow">Elma</a>’ by Yin Yin and ‘<a href="https://open.qobuz.com/track/47683549" target="_blank" rel="nofollow">Echoes</a>’ by Pink Floyd, shone and sounded vibrant without cutting through the rest of the soundstage.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3FmGWRVvPfwtvKddRTvSWD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/3FmGWRVvPfwtvKddRTvSWD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div  class="fancy-box"><div class="fancy_box-title">Hear me out...</div><div class="fancy_box_body"><p class="fancy-box__body-text">Naturally, I have a lot more to say about the sound quality, which is why my in-depth review is there! Check out my <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/bluetooth-speakers/soundboks-mix-review">Soundboks Mix review</a> for all the details.</p></div></div><p>None of that would matter, however, if the Mix didn't do what a party speaker is designed to do: make you dance to electronic tracks, and it manages to do so. The bass in ‘<a href="https://open.qobuz.com/track/400341345" target="_blank" rel="nofollow">Lights Burn Dimmer</a>’ by Fred Again and Jaime T sounded controlled and tight with the EQ set to Indoor. It didn't overpower the rest of the soundstage. Switching the EQ preset to Bass+ made me feel the song in my bones. It was loud and made the ground beneath me shake. I could feel my teeth chattering.</p><p>Turning the volume all the way up to 10 (as you might do if you're hosting a party outdoors) revealed a weakness, though. The bass was still room-filling, but it sounded wobbly and uncontrolled, like the Mix was struggling to keep up with it, or was feeling overwhelmed by it. Truth be told, that's not great news for a party speaker — especially for one that costs just under $800... which, very nicely, brings me to my next point.</p><h2 id="nowhere-to-hide-when-the-competition-is-so-good">Nowhere to hide when the competition is so good</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.15%;"><img id="WnGYtHHq6oV5ywBFiAwDSh" name="Marshall_Bromley_450_edit" alt="Marshall Bromley 450 party speaker" src="https://cdn.mos.cms.futurecdn.net/WnGYtHHq6oV5ywBFiAwDSh.jpg" mos="" align="middle" fullscreen="" width="2048" height="1150" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When I was testing the Soundboks Mix, I couldn't help but think to myself, "This speaker could be <em>the </em>best party speaker around... if it wasn't competing with the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Marshall Bromley 450</a>." The Bromley 450, Marshall's second-ever party speaker, is a top-notch product that doesn't skimp on features on sound quality. I've thoroughly tested that speaker too, and knowing what it offers for the same money has further highlighted the Mix's flaws to me.</p><div class="product"><a data-dimension112="7fe5201f-9c84-4929-82a7-8197fa2b803b" data-action="Deal Block" data-label="Bromley 450" data-dimension48="Bromley 450" data-dimension25="$799" href="https://www.amazon.com/Marshall-Bromley-Portable-Party-Speaker/dp/B0G62KZ7XD/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1453px;"><p class="vanilla-image-block" style="padding-top:103.23%;"><img id="HjgJt6n3dM7M8i2cDEqMue" name="Marshall Bromley 450" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/HjgJt6n3dM7M8i2cDEqMue.png" mos="" align="middle" fullscreen="" width="1453" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A plethora of wired and wireless connectivity options make the Marshall Bromley 450 not just a powerful speaker, but a stunning amp and karaoke machine as well. Music sounds glorious and highly detailed across genres, while Auracast works like a charm to hook up multiple speakers across rooms. Oh, and the reactive LED lighting helps with immersion. 40-hour battery life and IP55 waterproofing make it a winner.<a class="view-deal button" href="https://www.amazon.com/Marshall-Bromley-Portable-Party-Speaker/dp/B0G62KZ7XD/" target="_blank" rel="nofollow" data-dimension112="7fe5201f-9c84-4929-82a7-8197fa2b803b" data-action="Deal Block" data-label="Bromley 450" data-dimension48="Bromley 450" data-dimension25="$799">View Deal</a></p></div><p>One of the first things I noticed about the Mix was its lack of ports. All you get is a USB-C port for charging and a 3.5mm jack for wired playback. The Bromley 450 goes above and beyond here, with two XLR/6.35mm combo ports for microphones and acoustic/electric guitars, and RCA ports for keyboards and turntables. It essentially doubles (or triples) as an amplifier and a karaoke machine.</p><p>You've then got the Mix using an old version of Bluetooth: 4.2. That's not <em>bad, </em>per se, but it isn't good either for a speaker that came out in March 2026. Bluetooth 4.2 is an old version at this point. That means shorter range and no way to stream LE or hi-res audio, so you're stuck with the basic SBC codec.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XwbtGmTpNXUqgU9BykKqpg" name="Marshall_Bromley_450_edit-19" alt="Marshall Bromley 450 party speaker" src="https://cdn.mos.cms.futurecdn.net/XwbtGmTpNXUqgU9BykKqpg.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bromley 450 has yet another ace up its sleeve: 360° True Stereophonic surround sound, which means that regardless of where you're standing in a room, you'll hear the same exceptional sound. This makes listening to your favorite tunes more immersive, something the Mix can't achieve.</p><p>Combine all of this with the wobbly bass and you've got yourself a clear winner. The Mix costs <a href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/SOUNDBOKS-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">£599</a>, and the Bromley 450 retails for <a href="https://www.amazon.com/Marshall-Bromley-Portable-Party-Speaker/dp/B0G62KZ7XD/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.argos.co.uk/product/8397942" target="_blank" rel="nofollow">£549</a>, and for the same price, you get versatile connectivity options, better Bluetooth and LE audio, 360° sound, and even integrated stage lights that react to the music.</p><p>I know which speaker I'd personally buy. What do you think? Let me know in the comments below!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-XbQVgO"></div>                            </div>                            <script src="https://kwizly.com/embed/XbQVgO.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/soundboks-mix-review">I just tested Soundboks’ portable party speaker — and it’s so close to be being great</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">I just tested the ‘best party speaker of 2026’ — here's why you should buy the Marshall Bromley 450</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/forget-the-bose-s1-pro-the-brand-new-marshall-bromley-450-just-redefined-party-speakers">Forget Bose — there's no better party speaker on the planet than the brand-new Marshall Bromley 450</a></li></ul><div data-widget-type="multimodelreview" data-model-name="Marshall Bromley 450,Soundboks Mix"></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/i-thought-i-could-be-the-life-of-the-party-with-the-soundboks-mix-but-its-shortcomings-make-me-want-to-buy-another-party-speaker-instead</link>
                                                                            <description>
                            <![CDATA[ I've spent the last week testing the Soundboks Mix and while this party speaker is good, its closest competition blows it out of the water. Here's why. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">mkHvJHQGw27aP7ygozrSRj</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/JszLChisaE6N7vn4QJyMMD-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 20 Jun 2026 05:15:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/JszLChisaE6N7vn4QJyMMD-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Soundboks Mix party speaker]]></media:description>                                                            <media:text><![CDATA[A black Soundboks Mix party speaker]]></media:text>
                                <media:title type="plain"><![CDATA[A black Soundboks Mix party speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/JszLChisaE6N7vn4QJyMMD-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Small, portable Bluetooth speakers can sound really good, but let's be honest: if you're soundtracking a large gathering or party, a small one isn't going to cut it. You need a proper party speaker for that, one that gets incredibly loud and makes your guests feel like they're at a concert. How about one that hits a staggering 121dB, then?</p><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/soundboks-mix-review">Soundboks Mix</a> is perhaps the loudest speaker I've tested yet. It's loud, but even at the highest volume, music doesn't sound too distorted, and the treble and mids remain surprisingly clear. Bass is nothing short of Earth-shaking, and the room-filling sound leaves you drooling. But...</p><div class="product"><a data-dimension112="dc054b3e-61a7-4a29-b6c3-43def3c6fc70" data-action="Deal Block" data-label="Mix" data-dimension48="Mix" data-dimension25="$799" href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:661px;"><p class="vanilla-image-block" style="padding-top:141.45%;"><img id="sypUHzwyPGjW57FYtGkA6k" name="Soundboks-Mix-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/sypUHzwyPGjW57FYtGkA6k.jpg" mos="" align="middle" fullscreen="" width="661" height="935" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Soundboks Mix is the brand’s portable party speaker, and it packs impressive audio capabilities. Ground-shaking bass, crisp treble, and refined mids make listening to music and soundtracking a party a treat. It’s IP65-rated so it’s protected against the elements, and it comes with a user-friendly companion app for customizing the equalizer. However, there aren’t as many connection ports on it as its closest rival, and it uses Bluetooth 4.2.<a class="view-deal button" href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow" data-dimension112="dc054b3e-61a7-4a29-b6c3-43def3c6fc70" data-action="Deal Block" data-label="Mix" data-dimension48="Mix" data-dimension25="$799">View Deal</a></p></div><p>Yeah, there's always a "but." It's part and parcel of testing and reviewing products, because few (if any) are perfect. The Mix is no different. Even though I love its sound profile, its drawbacks have left me feeling shortchanged — and when $799 is on the line, you want bang for your buck. There is <em>one </em>speaker out there that's great value for money, and sadly, it ain't the Mix.</p><h2 id="shake-the-ground">Shake the ground</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Syt6GuUeRig4JF24bZU8cG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/Syt6GuUeRig4JF24bZU8cG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of the Soundboks Mix's biggest selling points is that it's get ridiculously loud. At a whopping 121dB, the Mix sounds loud enough to vibrate a glass off a desk if you're indoors, or shake the ground when you're outdoors. Metaphorically speaking, of course — I didn't <em>actually </em>manage to drop a glass off a countertop.</p><p>All that's to say is that you can certainly be the life of the party with the Mix. Whether you're head-banging to bass-heavy rock or dancing to electronic dance music, the Mix has you covered. Two 72W Class D amplifiers powered by a  10-inch woofer and a 1.2-inch dome tweeter cover a frequency response range of 40Hz to 20KHz — typical for a speaker of its demeanour.</p><p>Regardless of the tune I listened to, I was impressed. Lana Del Rey's vocals in ‘<a href="https://open.qobuz.com/album/gcsfc9y5o0or4" target="_blank" rel="nofollow">First Light</a>’ sat front and centre and sounded remarkably clear. The warmth, texture and emotional nuance were reproduced accurately, while subtle details in her delivery found themselves in the spotlight without sounding too clinical. The treble in rock tunes, like ‘<a href="https://open.qobuz.com/track/361933224" target="_blank" rel="nofollow">Elma</a>’ by Yin Yin and ‘<a href="https://open.qobuz.com/track/47683549" target="_blank" rel="nofollow">Echoes</a>’ by Pink Floyd, shone and sounded vibrant without cutting through the rest of the soundstage.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3FmGWRVvPfwtvKddRTvSWD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/3FmGWRVvPfwtvKddRTvSWD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><div  class="fancy-box"><div class="fancy_box-title">Hear me out...</div><div class="fancy_box_body"><p class="fancy-box__body-text">Naturally, I have a lot more to say about the sound quality, which is why my in-depth review is there! Check out my <a data-analytics-id="inline-link" href="https://www.tomsguide.com/audio/bluetooth-speakers/soundboks-mix-review">Soundboks Mix review</a> for all the details.</p></div></div><p>None of that would matter, however, if the Mix didn't do what a party speaker is designed to do: make you dance to electronic tracks, and it manages to do so. The bass in ‘<a href="https://open.qobuz.com/track/400341345" target="_blank" rel="nofollow">Lights Burn Dimmer</a>’ by Fred Again and Jaime T sounded controlled and tight with the EQ set to Indoor. It didn't overpower the rest of the soundstage. Switching the EQ preset to Bass+ made me feel the song in my bones. It was loud and made the ground beneath me shake. I could feel my teeth chattering.</p><p>Turning the volume all the way up to 10 (as you might do if you're hosting a party outdoors) revealed a weakness, though. The bass was still room-filling, but it sounded wobbly and uncontrolled, like the Mix was struggling to keep up with it, or was feeling overwhelmed by it. Truth be told, that's not great news for a party speaker — especially for one that costs just under $800... which, very nicely, brings me to my next point.</p><h2 id="nowhere-to-hide-when-the-competition-is-so-good">Nowhere to hide when the competition is so good</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.15%;"><img id="WnGYtHHq6oV5ywBFiAwDSh" name="Marshall_Bromley_450_edit" alt="Marshall Bromley 450 party speaker" src="https://cdn.mos.cms.futurecdn.net/WnGYtHHq6oV5ywBFiAwDSh.jpg" mos="" align="middle" fullscreen="" width="2048" height="1150" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When I was testing the Soundboks Mix, I couldn't help but think to myself, "This speaker could be <em>the </em>best party speaker around... if it wasn't competing with the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Marshall Bromley 450</a>." The Bromley 450, Marshall's second-ever party speaker, is a top-notch product that doesn't skimp on features on sound quality. I've thoroughly tested that speaker too, and knowing what it offers for the same money has further highlighted the Mix's flaws to me.</p><div class="product"><a data-dimension112="7fe5201f-9c84-4929-82a7-8197fa2b803b" data-action="Deal Block" data-label="Bromley 450" data-dimension48="Bromley 450" data-dimension25="$799" href="https://www.amazon.com/Marshall-Bromley-Portable-Party-Speaker/dp/B0G62KZ7XD/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1453px;"><p class="vanilla-image-block" style="padding-top:103.23%;"><img id="HjgJt6n3dM7M8i2cDEqMue" name="Marshall Bromley 450" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/HjgJt6n3dM7M8i2cDEqMue.png" mos="" align="middle" fullscreen="" width="1453" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A plethora of wired and wireless connectivity options make the Marshall Bromley 450 not just a powerful speaker, but a stunning amp and karaoke machine as well. Music sounds glorious and highly detailed across genres, while Auracast works like a charm to hook up multiple speakers across rooms. Oh, and the reactive LED lighting helps with immersion. 40-hour battery life and IP55 waterproofing make it a winner.<a class="view-deal button" href="https://www.amazon.com/Marshall-Bromley-Portable-Party-Speaker/dp/B0G62KZ7XD/" target="_blank" rel="nofollow" data-dimension112="7fe5201f-9c84-4929-82a7-8197fa2b803b" data-action="Deal Block" data-label="Bromley 450" data-dimension48="Bromley 450" data-dimension25="$799">View Deal</a></p></div><p>One of the first things I noticed about the Mix was its lack of ports. All you get is a USB-C port for charging and a 3.5mm jack for wired playback. The Bromley 450 goes above and beyond here, with two XLR/6.35mm combo ports for microphones and acoustic/electric guitars, and RCA ports for keyboards and turntables. It essentially doubles (or triples) as an amplifier and a karaoke machine.</p><p>You've then got the Mix using an old version of Bluetooth: 4.2. That's not <em>bad, </em>per se, but it isn't good either for a speaker that came out in March 2026. Bluetooth 4.2 is an old version at this point. That means shorter range and no way to stream LE or hi-res audio, so you're stuck with the basic SBC codec.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2048px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XwbtGmTpNXUqgU9BykKqpg" name="Marshall_Bromley_450_edit-19" alt="Marshall Bromley 450 party speaker" src="https://cdn.mos.cms.futurecdn.net/XwbtGmTpNXUqgU9BykKqpg.jpg" mos="" align="middle" fullscreen="" width="2048" height="1152" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Bromley 450 has yet another ace up its sleeve: 360° True Stereophonic surround sound, which means that regardless of where you're standing in a room, you'll hear the same exceptional sound. This makes listening to your favorite tunes more immersive, something the Mix can't achieve.</p><p>Combine all of this with the wobbly bass and you've got yourself a clear winner. The Mix costs <a href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/SOUNDBOKS-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">£599</a>, and the Bromley 450 retails for <a href="https://www.amazon.com/Marshall-Bromley-Portable-Party-Speaker/dp/B0G62KZ7XD/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.argos.co.uk/product/8397942" target="_blank" rel="nofollow">£549</a>, and for the same price, you get versatile connectivity options, better Bluetooth and LE audio, 360° sound, and even integrated stage lights that react to the music.</p><p>I know which speaker I'd personally buy. What do you think? Let me know in the comments below!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-XbQVgO"></div>                            </div>                            <script src="https://kwizly.com/embed/XbQVgO.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/soundboks-mix-review">I just tested Soundboks’ portable party speaker — and it’s so close to be being great</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">I just tested the ‘best party speaker of 2026’ — here's why you should buy the Marshall Bromley 450</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/forget-the-bose-s1-pro-the-brand-new-marshall-bromley-450-just-redefined-party-speakers">Forget Bose — there's no better party speaker on the planet than the brand-new Marshall Bromley 450</a></li></ul><div data-widget-type="multimodelreview" data-model-name="Marshall Bromley 450,Soundboks Mix"></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just tested Soundboks’ portable party speaker — and it’s so close to be being great ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Sometimes, a small speaker isn’t loud enough to soundtrack an evening with friends or a big party. You need a party speaker for that, and the Soundboks Mix is a solid choice. Soundboks’ portable party speaker is built extremely well and is protected against the elements, thanks to its IP65 rating. The sound? Nearly exquisite. The Mix delivers powerful, Earth-shaking bass, vibrant treble, and precise, balanced mids.</p><p>But is it the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> for the money? It costs $799 and features some noteworthy flaws, such as wobbly bass at high volumes, Bluetooth 4.2 only, no 360° sound, and more. If those are compromises you’re willing to make, then the Mix could be perfect for you. Trust me, it sounds really, <em>really </em>good.</p><p>For the complete breakdown, read my full Soundboks Mix review.</p><h2 class="article-body__section" id="section-soundboks-mix-review-specs"><span>Soundboks Mix review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/SOUNDBOKS-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">£599</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>IP65</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 4.2, 3.5mm wired</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes, up to 5</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>2x 72W Class D amplifiers, 1x 10-inch woofer, 1x 1.2-inch dome tweeter</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>40Hz-20KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>40 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>18.9 x 13.4 x 10.6 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>21.38lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-soundboks-mix-review-price-availability"><span>Soundboks Mix review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Syt6GuUeRig4JF24bZU8cG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/Syt6GuUeRig4JF24bZU8cG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Launched in March 2026, the Soundboks Mix is the <a href="https://www.tomsguide.com/audio/soundboks-4-review">Soundboks 4</a>’s smaller sibling, and it effectively replaces the Soundboks Go as the brand’s most portable party speaker — it’s priced as such too. While the 4 costs $999, the Mix retails for <a href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/SOUNDBOKS-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">£599 at Amazon</a>. Given how loud it gets, the Mix is a great party speaker for anyone who doesn’t want to spend nearly a grand on the bigger model.</p><p>The Mix is Soundboks’ answer to the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Marshall Bromley 450</a> ($799) and the JBL PartyBox 520 ($879). It’s got the specs and sound to go toe-to-toe with the Bromley 450, too, but is it worth buying instead of the Marshall? I've reviewed both, so let’s find out.</p><h2 class="article-body__section" id="section-soundboks-mix-review-design"><span>Soundboks Mix review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JszLChisaE6N7vn4QJyMMD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/JszLChisaE6N7vn4QJyMMD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Soundboks Mix looks very similar to the Soundboks 4 — similar to how the Marshall Bromley 450 and <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Marshall Bromley 750</a> ($1,299) share design philosophies. You’re getting bang for your buck here as the Mix feels very well-built, featuring an ABS and polycarbonate cabinet, silicone ball corners to protect it from getting damaged, and a detachable steel grill. All in all, it feels sturdy and durable.</p><p>The Mix doesn’t sport any garish LED lighting that makes it look tacky, like on the JBL PartyBox 520 — although I do wish it had some integrated stage lights, like the Bromley 450 does. Branding is fairly minimal too, with the logo sitting in the top left corner. You can buy the speaker in black only, which is no bad thing; the Bromley 450 and PartyBox 520 come in single colors too.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="GYJcwZCcUHcGHYGwX8DUDD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/GYJcwZCcUHcGHYGwX8DUDD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As Soundboks’ most compact party speaker, the Mix doesn’t feel too heavy and doesn’t take up much space either. It measures 18.9 x 13.4 x 10.6 inches and weighs 21.38lbs, so it’s about 5lbs lighter than the Bromley 450. To move it around, you can use the built-in carry handle, which is fine, but because there’s only one located in one corner, the Mix feels a little unwieldy to move around. This problem could have been easily solved by installing a suitcase-style handle smack dab in the middle of its side, <em>à la</em> Bromley 450.</p><p>But hey, you can buy a strap to turn the Mix into a backpack… for $59. And it doesn’t come with a charger... which you can also buy separately for $59. The Mix feels to speakers what EA is to gaming: full of microtransactions.</p><h2 id="weatherproofing-4">Weatherproofing</h2><p>If you want a rugged party speaker that you can use outdoors without worrying too much about it getting rained on, the Mix is a great choice. The speaker is IP65-rated which means it’s fully dust-tight and protected against rain and moisture. That’s slightly better than the Marshall Bromley 450 which is IP55-rated. When I was testing the Mix outdoors, it started drizzling and knowing that the speaker was IP65-rated reassured me.</p><h2 id="controls-12">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3FmGWRVvPfwtvKddRTvSWD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/3FmGWRVvPfwtvKddRTvSWD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There isn’t a lot going on in terms of controls on the Mix. All the physical buttons and volume dial are located on the speaker’s right edge, and these include the volume knob, TeamUp button for wirelessly linking up to five Soundboks speakers together, and the power button for, well, turning the speaker on and off. That’s about it. The buttons give good tactile feedback when pressed, and are fairly straightforward to understand.</p><h2 id="connectivity-9">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SVDVY2PHpuqyycvZDw6YBD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/SVDVY2PHpuqyycvZDw6YBD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Mix utilizes Bluetooth 4.2 to connect wirelessly to your smartphone, laptop or other devices, and it features multipoint connectivity for simultaneously connecting up to two devices. Bluetooth 4.2 is disappointing, though, as it means there’s limited codec support, so you only get the standard SBC codec. No AAC and no LE audio, unfortunately.</p><p>This older version also limits the speaker’s maximum operating range to 40m only. Not great for a party speaker. What if you’re soundtracking a big gathering and you’re far away from the speaker, and the connection drops when you move out of range? That’s one way to kill the mood.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yMsmMvtNJcyKQykMqrFSnD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/yMsmMvtNJcyKQykMqrFSnD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In comparison, the Marshall Bromley 450 utilizes the newer Bluetooth 5.3 with support for AAC and LC3, and sports many more connectivity options, such as RCA and two XLR/6.35mm combo jacks for plugging in instruments or microphones. One thing the Mix and Bromley 450 have in common, though, is 3.5mm wired playback. But if you want a versatile party speaker, I’d choose the Bromley 450 any day.</p><p>Like I alluded to earlier, there's a TeamUp button on the Mix for wirelessly connecting up to five Soundboks speakers. There's also a Bond button with arrows, located right under the TeamUp button. When it lights up in green, it means the speaker is connected to another speaker it has been linked to in the past. You can also become the "Host" when other speakers are connected to yours, and control them via the smartphone app (more on that shortly).</p><h2 class="article-body__section" id="section-soundboks-mix-review-sound-quality"><span>Soundboks Mix review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jhMuR9CbKmAXM8EQq9xeXD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/jhMuR9CbKmAXM8EQq9xeXD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s no dearth of tech squeezed into the Soundboks Mix’s portable body. It packs two 72W Class D amplifiers powered by a 10-inch woofer and a 1.2-inch dome tweeter. It can go all the way up to 121dB — a little louder than the Marshall Bromley 450 (100dB). The Mix covers a frequency response range of 40Hz to 20KHz, which is typical for party speakers as it delivers crisp highs and deep bass, perfect for pop, rock, EDM, what-have-you. I listened to my usual mix of testing songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res audio.</p><p>To test the Mix’s midrange performance, I listened to ‘<a href="https://open.qobuz.com/album/gcsfc9y5o0or4" target="_blank" rel="nofollow">First Light</a>’ by Lana Del Rey, and I was instantly impressed. Lana’s vocals sat front and centre with remarkable clarity, and the speaker captured and accurately reproduced the warmth, texture and emotional nuance that define the James Bond theme song. Subtle details in her delivery, such as her short, sharp breaths, shone without sounding too clinical or like they were piercing through the soundscape. I also appreciated the Mix preserving the tonal balance even at the highest volume — the track never once sounded distorted, and it still sounded engaging and cinematic.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EiD67JXUNuFZoSMpLha6mG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/EiD67JXUNuFZoSMpLha6mG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s nothing the Mix can’t handle, and it manages to deliver vibrant treble without making it sound too overzealous. The psychedelic/electronic guitar notes landed with impressive clarity and shimmered in ‘<a href="https://open.qobuz.com/track/361933224" target="_blank" rel="nofollow">Elma</a>’ by Yin Yin, while the drums’ hi-hats added depth to the track without becoming harsh or fatiguing.</p><p>Similarly, in Pink Floyd’s ‘<a href="https://open.qobuz.com/track/47683549" target="_blank" rel="nofollow">Echoes</a>,’ I thoroughly enjoyed the delicate balance between cymbal work and the ambient or atmospheric sound effects. The Mix added a brilliant airiness to the track, making me feel like I was there with the band as they <a href="https://www.youtube.com/watch?v=JQ2pTamaqQ4" target="_blank" rel="nofollow">recorded the song at Pompeii</a>.</p><p>But this is a party speaker, after all, so I had to test it by listening to an electronic track you can dance to. ‘<a href="https://open.qobuz.com/track/400341345" target="_blank" rel="nofollow">Lights Burn Dimmer</a>’ by Fred Again and Jaime T highlights the Mix’s greatest strength: bass. With the EQ set to Indoor, the bass sounds very controlled and tight, giving the track a great backbone without overpowering the rest of the soundstage. I then tried the Bass+ EQ preset which turns the bass all the way up to 10.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:962px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Syt6GuUeRig4JF24bZU8cG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/v2/t:0,l:524,cw:962,ch:541,q:80/Syt6GuUeRig4JF24bZU8cG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>While the song still sounded powerful and the bass was room-filling, I couldn’t help but notice that the bassline sounded wobbly, like it was too much for the Mix to handle. This was indoors so I tried this preset outdoors, and the bass still sounded slightly muddy — an issue I never encountered when testing the Marshall Bromley 450.</p><p>It’s also important to note that the Mix doesn’t feature 360° surround sound, which means that you can only really appreciate music when standing directly in front of the speaker. If you want a more immersive listening experience, and want your party guests to be able to hear your tunes regardless of where they’re standing, I’d recommend the Bromley 450 which features True Stereophonic 360° sound.</p><p><em>Listen along to my review with the Soundboks Mix testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/65793089?zone=GB-en"></iframe><h2 class="article-body__section" id="section-soundboks-mix-review-app"><span>Soundboks Mix review: App</span></h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/ogr744HPDqdbhZrAqcJNzX.jpg" alt="Soundboks mobile app screenshots" /><figcaption><small role="credit">Soundboks / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/74Rp82WHWWx762bPoNj7iX.jpg" alt="Soundboks mobile app screenshots" /><figcaption><small role="credit">Soundboks / Tom's Guide</small></figcaption></figure></figure><p>The Soundboks Mix is accompanied by the brand’s app for all its speakers, and it’s available for free on <a href="https://soundboks.co.uk/pages/app" target="_blank" rel="nofollow">iOS and Android</a>. This is one area in which the Mix upstages the Marshall Bromley 450. The Bromley 450’s app is decent but not great, and the Mix offers a lot more in terms of customization.</p><p>Via the user-friendly app, you can make adjustments to the five-band custom equalizer, choose one of three EQ presets, control other Soundboks speakers you’ve paired with the Mix, control the volume, and, of course, update the firmware. I also like that the EQ presets have quirky descriptions. For instance, the Bass+ preset is described as, “More bass, just like you wanted. Yes, you.” I dig it.</p><h2 class="article-body__section" id="section-soundboks-mix-review-battery-life"><span>Soundboks Mix review: Battery life</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Zcz5bn23Qc9mujvj2LWZUD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/Zcz5bn23Qc9mujvj2LWZUD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Soundboks Mix offers superb battery life as it can last up to 40 hours between charges. Battery life will naturally depend on the volume and EQ presets, and happily, the app tells you how long the speaker will last when certain EQ presets are active. If you’re playing at full volume or have the Bass+ preset enabled, the battery life drops to eight hours. You can use any USB-C cable to charge the Mix.</p><p>Both the Marshall Bromley 750 and the Marshall Bromley 450 also pack 40-hour batteries. The Mix upstages the JBL PartyBox 520’s 15-hour and the Sony ULT Tower 9’s 25-hour batteries, too.</p><p>Similar to both the Bromleys, the Mix’s battery pack is removable and it can even be used to charge your smartphone, laptop and other devices. This is a handy feature to have especially if you’re out and about and not near a charging outlet.</p><h2 class="article-body__section" id="section-soundboks-mix-review-verdict"><span>Soundboks Mix review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="i9BiXXNabWbv4Mg8hxpnPD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/i9BiXXNabWbv4Mg8hxpnPD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Soundboks Mix isn’t without its compromises, but when it comes to sound quality, it still delivers a wonderful listening experience. Party speakers need to put out big sound, and the Mix can get incredibly loud without distortion. Earth-shaking bass is balanced well with detailed treble and mids, and though the bass can get a little wobbly at high volumes, it’s still enjoyable.</p><p>Designed to withstand the elements, the Mix is IP65-rated and is reinforced to stand the test of time. That said, for the same price of $799, the Marshall Bromley 450 offers better value for money. More ports for hooking up instruments, better connectivity thanks to Bluetooth 5.3, and 360° sound.</p><p>If all you want is full-bodied sound, the Mix is a great choice, but the extra versatility of similarly-priced rivals stops it achieving one of our very highest scores.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/soundboks-mix-review</link>
                                                                            <description>
                            <![CDATA[ The Soundboks Mix is a powerful and incredibly loud party speaker, which delivers ground-shaking bass and 40 hours of battery life. But it has some flaws. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">vArdHPQmMCrCNNhQyKU75g</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/Syt6GuUeRig4JF24bZU8cG-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 19 Jun 2026 10:10:42 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/Syt6GuUeRig4JF24bZU8cG-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Soundboks Mix party speaker]]></media:description>                                                            <media:text><![CDATA[A black Soundboks Mix party speaker]]></media:text>
                                <media:title type="plain"><![CDATA[A black Soundboks Mix party speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/Syt6GuUeRig4JF24bZU8cG-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Sometimes, a small speaker isn’t loud enough to soundtrack an evening with friends or a big party. You need a party speaker for that, and the Soundboks Mix is a solid choice. Soundboks’ portable party speaker is built extremely well and is protected against the elements, thanks to its IP65 rating. The sound? Nearly exquisite. The Mix delivers powerful, Earth-shaking bass, vibrant treble, and precise, balanced mids.</p><p>But is it the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> for the money? It costs $799 and features some noteworthy flaws, such as wobbly bass at high volumes, Bluetooth 4.2 only, no 360° sound, and more. If those are compromises you’re willing to make, then the Mix could be perfect for you. Trust me, it sounds really, <em>really </em>good.</p><p>For the complete breakdown, read my full Soundboks Mix review.</p><h2 class="article-body__section" id="section-soundboks-mix-review-specs"><span>Soundboks Mix review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/SOUNDBOKS-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">£599</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>IP65</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 4.2, 3.5mm wired</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>Yes, up to 5</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>2x 72W Class D amplifiers, 1x 10-inch woofer, 1x 1.2-inch dome tweeter</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>40Hz-20KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>40 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>18.9 x 13.4 x 10.6 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>21.38lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-soundboks-mix-review-price-availability"><span>Soundboks Mix review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Syt6GuUeRig4JF24bZU8cG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/Syt6GuUeRig4JF24bZU8cG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Launched in March 2026, the Soundboks Mix is the <a href="https://www.tomsguide.com/audio/soundboks-4-review">Soundboks 4</a>’s smaller sibling, and it effectively replaces the Soundboks Go as the brand’s most portable party speaker — it’s priced as such too. While the 4 costs $999, the Mix retails for <a href="https://www.amazon.com/Soundboks-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">$799</a> / <a href="https://www.amazon.co.uk/SOUNDBOKS-Mix-Weatherproof-Rechargeable-Ultra-Loud/dp/B0GLP3S87Y/" target="_blank" rel="nofollow">£599 at Amazon</a>. Given how loud it gets, the Mix is a great party speaker for anyone who doesn’t want to spend nearly a grand on the bigger model.</p><p>The Mix is Soundboks’ answer to the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-450-review">Marshall Bromley 450</a> ($799) and the JBL PartyBox 520 ($879). It’s got the specs and sound to go toe-to-toe with the Bromley 450, too, but is it worth buying instead of the Marshall? I've reviewed both, so let’s find out.</p><h2 class="article-body__section" id="section-soundboks-mix-review-design"><span>Soundboks Mix review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JszLChisaE6N7vn4QJyMMD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/JszLChisaE6N7vn4QJyMMD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Soundboks Mix looks very similar to the Soundboks 4 — similar to how the Marshall Bromley 450 and <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-bromley-750-review">Marshall Bromley 750</a> ($1,299) share design philosophies. You’re getting bang for your buck here as the Mix feels very well-built, featuring an ABS and polycarbonate cabinet, silicone ball corners to protect it from getting damaged, and a detachable steel grill. All in all, it feels sturdy and durable.</p><p>The Mix doesn’t sport any garish LED lighting that makes it look tacky, like on the JBL PartyBox 520 — although I do wish it had some integrated stage lights, like the Bromley 450 does. Branding is fairly minimal too, with the logo sitting in the top left corner. You can buy the speaker in black only, which is no bad thing; the Bromley 450 and PartyBox 520 come in single colors too.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="GYJcwZCcUHcGHYGwX8DUDD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/GYJcwZCcUHcGHYGwX8DUDD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As Soundboks’ most compact party speaker, the Mix doesn’t feel too heavy and doesn’t take up much space either. It measures 18.9 x 13.4 x 10.6 inches and weighs 21.38lbs, so it’s about 5lbs lighter than the Bromley 450. To move it around, you can use the built-in carry handle, which is fine, but because there’s only one located in one corner, the Mix feels a little unwieldy to move around. This problem could have been easily solved by installing a suitcase-style handle smack dab in the middle of its side, <em>à la</em> Bromley 450.</p><p>But hey, you can buy a strap to turn the Mix into a backpack… for $59. And it doesn’t come with a charger... which you can also buy separately for $59. The Mix feels to speakers what EA is to gaming: full of microtransactions.</p><h2 id="weatherproofing-4">Weatherproofing</h2><p>If you want a rugged party speaker that you can use outdoors without worrying too much about it getting rained on, the Mix is a great choice. The speaker is IP65-rated which means it’s fully dust-tight and protected against rain and moisture. That’s slightly better than the Marshall Bromley 450 which is IP55-rated. When I was testing the Mix outdoors, it started drizzling and knowing that the speaker was IP65-rated reassured me.</p><h2 id="controls-12">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3FmGWRVvPfwtvKddRTvSWD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/3FmGWRVvPfwtvKddRTvSWD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There isn’t a lot going on in terms of controls on the Mix. All the physical buttons and volume dial are located on the speaker’s right edge, and these include the volume knob, TeamUp button for wirelessly linking up to five Soundboks speakers together, and the power button for, well, turning the speaker on and off. That’s about it. The buttons give good tactile feedback when pressed, and are fairly straightforward to understand.</p><h2 id="connectivity-9">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SVDVY2PHpuqyycvZDw6YBD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/SVDVY2PHpuqyycvZDw6YBD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Mix utilizes Bluetooth 4.2 to connect wirelessly to your smartphone, laptop or other devices, and it features multipoint connectivity for simultaneously connecting up to two devices. Bluetooth 4.2 is disappointing, though, as it means there’s limited codec support, so you only get the standard SBC codec. No AAC and no LE audio, unfortunately.</p><p>This older version also limits the speaker’s maximum operating range to 40m only. Not great for a party speaker. What if you’re soundtracking a big gathering and you’re far away from the speaker, and the connection drops when you move out of range? That’s one way to kill the mood.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yMsmMvtNJcyKQykMqrFSnD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/yMsmMvtNJcyKQykMqrFSnD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In comparison, the Marshall Bromley 450 utilizes the newer Bluetooth 5.3 with support for AAC and LC3, and sports many more connectivity options, such as RCA and two XLR/6.35mm combo jacks for plugging in instruments or microphones. One thing the Mix and Bromley 450 have in common, though, is 3.5mm wired playback. But if you want a versatile party speaker, I’d choose the Bromley 450 any day.</p><p>Like I alluded to earlier, there's a TeamUp button on the Mix for wirelessly connecting up to five Soundboks speakers. There's also a Bond button with arrows, located right under the TeamUp button. When it lights up in green, it means the speaker is connected to another speaker it has been linked to in the past. You can also become the "Host" when other speakers are connected to yours, and control them via the smartphone app (more on that shortly).</p><h2 class="article-body__section" id="section-soundboks-mix-review-sound-quality"><span>Soundboks Mix review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jhMuR9CbKmAXM8EQq9xeXD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/jhMuR9CbKmAXM8EQq9xeXD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s no dearth of tech squeezed into the Soundboks Mix’s portable body. It packs two 72W Class D amplifiers powered by a 10-inch woofer and a 1.2-inch dome tweeter. It can go all the way up to 121dB — a little louder than the Marshall Bromley 450 (100dB). The Mix covers a frequency response range of 40Hz to 20KHz, which is typical for party speakers as it delivers crisp highs and deep bass, perfect for pop, rock, EDM, what-have-you. I listened to my usual mix of testing songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res audio.</p><p>To test the Mix’s midrange performance, I listened to ‘<a href="https://open.qobuz.com/album/gcsfc9y5o0or4" target="_blank" rel="nofollow">First Light</a>’ by Lana Del Rey, and I was instantly impressed. Lana’s vocals sat front and centre with remarkable clarity, and the speaker captured and accurately reproduced the warmth, texture and emotional nuance that define the James Bond theme song. Subtle details in her delivery, such as her short, sharp breaths, shone without sounding too clinical or like they were piercing through the soundscape. I also appreciated the Mix preserving the tonal balance even at the highest volume — the track never once sounded distorted, and it still sounded engaging and cinematic.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EiD67JXUNuFZoSMpLha6mG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/EiD67JXUNuFZoSMpLha6mG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>There’s nothing the Mix can’t handle, and it manages to deliver vibrant treble without making it sound too overzealous. The psychedelic/electronic guitar notes landed with impressive clarity and shimmered in ‘<a href="https://open.qobuz.com/track/361933224" target="_blank" rel="nofollow">Elma</a>’ by Yin Yin, while the drums’ hi-hats added depth to the track without becoming harsh or fatiguing.</p><p>Similarly, in Pink Floyd’s ‘<a href="https://open.qobuz.com/track/47683549" target="_blank" rel="nofollow">Echoes</a>,’ I thoroughly enjoyed the delicate balance between cymbal work and the ambient or atmospheric sound effects. The Mix added a brilliant airiness to the track, making me feel like I was there with the band as they <a href="https://www.youtube.com/watch?v=JQ2pTamaqQ4" target="_blank" rel="nofollow">recorded the song at Pompeii</a>.</p><p>But this is a party speaker, after all, so I had to test it by listening to an electronic track you can dance to. ‘<a href="https://open.qobuz.com/track/400341345" target="_blank" rel="nofollow">Lights Burn Dimmer</a>’ by Fred Again and Jaime T highlights the Mix’s greatest strength: bass. With the EQ set to Indoor, the bass sounds very controlled and tight, giving the track a great backbone without overpowering the rest of the soundstage. I then tried the Bass+ EQ preset which turns the bass all the way up to 10.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:962px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Syt6GuUeRig4JF24bZU8cG" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/v2/t:0,l:524,cw:962,ch:541,q:80/Syt6GuUeRig4JF24bZU8cG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>While the song still sounded powerful and the bass was room-filling, I couldn’t help but notice that the bassline sounded wobbly, like it was too much for the Mix to handle. This was indoors so I tried this preset outdoors, and the bass still sounded slightly muddy — an issue I never encountered when testing the Marshall Bromley 450.</p><p>It’s also important to note that the Mix doesn’t feature 360° surround sound, which means that you can only really appreciate music when standing directly in front of the speaker. If you want a more immersive listening experience, and want your party guests to be able to hear your tunes regardless of where they’re standing, I’d recommend the Bromley 450 which features True Stereophonic 360° sound.</p><p><em>Listen along to my review with the Soundboks Mix testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/65793089?zone=GB-en"></iframe><h2 class="article-body__section" id="section-soundboks-mix-review-app"><span>Soundboks Mix review: App</span></h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/ogr744HPDqdbhZrAqcJNzX.jpg" alt="Soundboks mobile app screenshots" /><figcaption><small role="credit">Soundboks / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/74Rp82WHWWx762bPoNj7iX.jpg" alt="Soundboks mobile app screenshots" /><figcaption><small role="credit">Soundboks / Tom's Guide</small></figcaption></figure></figure><p>The Soundboks Mix is accompanied by the brand’s app for all its speakers, and it’s available for free on <a href="https://soundboks.co.uk/pages/app" target="_blank" rel="nofollow">iOS and Android</a>. This is one area in which the Mix upstages the Marshall Bromley 450. The Bromley 450’s app is decent but not great, and the Mix offers a lot more in terms of customization.</p><p>Via the user-friendly app, you can make adjustments to the five-band custom equalizer, choose one of three EQ presets, control other Soundboks speakers you’ve paired with the Mix, control the volume, and, of course, update the firmware. I also like that the EQ presets have quirky descriptions. For instance, the Bass+ preset is described as, “More bass, just like you wanted. Yes, you.” I dig it.</p><h2 class="article-body__section" id="section-soundboks-mix-review-battery-life"><span>Soundboks Mix review: Battery life</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Zcz5bn23Qc9mujvj2LWZUD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/Zcz5bn23Qc9mujvj2LWZUD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Soundboks Mix offers superb battery life as it can last up to 40 hours between charges. Battery life will naturally depend on the volume and EQ presets, and happily, the app tells you how long the speaker will last when certain EQ presets are active. If you’re playing at full volume or have the Bass+ preset enabled, the battery life drops to eight hours. You can use any USB-C cable to charge the Mix.</p><p>Both the Marshall Bromley 750 and the Marshall Bromley 450 also pack 40-hour batteries. The Mix upstages the JBL PartyBox 520’s 15-hour and the Sony ULT Tower 9’s 25-hour batteries, too.</p><p>Similar to both the Bromleys, the Mix’s battery pack is removable and it can even be used to charge your smartphone, laptop and other devices. This is a handy feature to have especially if you’re out and about and not near a charging outlet.</p><h2 class="article-body__section" id="section-soundboks-mix-review-verdict"><span>Soundboks Mix review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="i9BiXXNabWbv4Mg8hxpnPD" name="Soundboks_Mix_" alt="A black Soundboks Mix party speaker" src="https://cdn.mos.cms.futurecdn.net/i9BiXXNabWbv4Mg8hxpnPD.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Soundboks Mix isn’t without its compromises, but when it comes to sound quality, it still delivers a wonderful listening experience. Party speakers need to put out big sound, and the Mix can get incredibly loud without distortion. Earth-shaking bass is balanced well with detailed treble and mids, and though the bass can get a little wobbly at high volumes, it’s still enjoyable.</p><p>Designed to withstand the elements, the Mix is IP65-rated and is reinforced to stand the test of time. That said, for the same price of $799, the Marshall Bromley 450 offers better value for money. More ports for hooking up instruments, better connectivity thanks to Bluetooth 5.3, and 360° sound.</p><p>If all you want is full-bodied sound, the Mix is a great choice, but the extra versatility of similarly-priced rivals stops it achieving one of our very highest scores.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I test Bluetooth speakers for a living and this $50 offering rivals big names like JBL and Bose — here's what I like and don't like about the Tribit StormBox Micro 3 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>As a reviews writer here at Tom's Guide, I get my hands on a lot of the latest tech, including some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a>. And funnily enough, some of my favorites have been some of the cheapest offerings. </p><p>Tribit is a budget audio brand, but its Bluetooth speakers always turn out to be some of my favorites. I recently tested the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit StormBox Micro 3,</a> and it totally earns it's 4.5 star rating. </p><p>The speaker is only $64, and can often be found on sale for even less. Like right now, it's down to <a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S">$49 at Amazon U.S</a>.</p><div class="product"><a data-dimension112="f05d8fe3-c877-4865-9d92-522d03a24fab" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="RS3PDefgkukwjHkV9sCy9M" name="tribit-stormbox-micro-3-portable-bluetoo-9e0eed43-f6d4-445f-be8d-6cac88998b28.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/RS3PDefgkukwjHkV9sCy9M.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Tribit StormBox Micro 3 offers an IP68 rating, a companion app for EQ customization, and simple controls that make the speaker a breeze to use. It has 24 hours of battery life and can even be used as a portable charger. But most importantly, the audio performance really is excellent.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="f05d8fe3-c877-4865-9d92-522d03a24fab" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49">View Deal</a></p></div><h2 id="excellent-audio-performance">Excellent audio performance</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AdztFXRYm2qRnPoAbredeU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/AdztFXRYm2qRnPoAbredeU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Of course, sound is the most important thing to consider when buying a new Bluetooth speaker, and the Tribit StormBox Micro 3 nails it.  For such a small speaker, it creates a big sound. </p><p>And when the BassX setting is deployed, you have home-filling sound. I love using this feature in the house or just in the backyard while taking care of some chores. You can also switch up the EQ settings in the Tribit companion app, but the BassX button on the speaker makes switching up the sound incredibly easy, and it suits most music genres. </p><h2 id="top-choice-for-durability">Top choice for durability</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5C4rm2BEgQpXe64xtKfNaU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/5C4rm2BEgQpXe64xtKfNaU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>With a durability rating of IP68, this speaker can take on anything. With this rating, the speaker can actually withstand water submersion and is completely dust-tight. </p><p>You don't need to worry about dropping it, either, as it's been rated to survive falls from as much as four feet. I'm the world's worst butterfinger, so I did end up dropping the speaker a few times, and it was completely fine.  </p><h2 id="all-day-battery">All-day battery</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9JFp65GWDw98dPHu7u9yXU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9JFp65GWDw98dPHu7u9yXU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The 24-hour battery life is one of the best things about this speaker. It's double the battery life of some competitors, and it doesn't suffer too much at high volumes. </p><p>If using the BassX settings, the battery drains faster, but I was still able to get a good 18 hours, which is very impressive. </p><h2 id="not-exactly-micro">Not exactly "micro"</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dLJQsthExUW6jmoyP5wHVU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/dLJQsthExUW6jmoyP5wHVU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Though the StormBox Micro 3 is small, Tribit actually makes smaller speakers than this. The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a>, for example, is a lot smaller, and I find it far easier to carry around with me. </p><p>It's also surprisingly heavy, but that's down to the speaker being magnetic. This feature is incredibly convenient, and in some ways makes it worth the extra weight. </p><p>To find out more about the Bluetooth speaker and what it can do, you can read my full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit StromBox Micro 3 review</a>.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review#section-tribit-stormbox-micro-3-review-sound-quality"><strong>Tribit has done it again — the brand-new StormBox Micro 3 rivals JBL for under $70</strong></a></li><li><a href="https://www.tomsguide.com/audio/you-can-take-a-portable-bluetooth-speaker-with-you-camping-but-heres-why-its-not-the-best-idea-to-use-it-on-your-hike"><strong>You can take a portable Bluetooth speaker with you camping, but here's why it's not the best idea to use it on your hike</strong></a></li><li><a href="https://www.tomsguide.com/ai/ever-wonder-why-chatgpt-is-free-the-answer-is-far-more-calculated-than-you-might-think"><strong>Ever wonder why ChatGPT is free? The answer is far more calculated than you might think</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/i-test-bluetooth-speakers-for-a-living-and-this-sub-usd70-offering-from-tribit-rivals-big-names-like-jbl-and-bose-heres-what-i-like-and-dont-like-about-it</link>
                                                                            <description>
                            <![CDATA[ After using the Tribit StormBox Micro 3 for a few weeks, it's become my go-to speaker for indoors and in the backyard. Here's why. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">6yifRuMeSEmBMyhPtpkDyD</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/mpYmpr4wAMUdFoxsr362Cg-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 15 Jun 2026 05:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ ashley.thieme@futurenet.com (Ashley Thieme) ]]></author>                    <dc:creator><![CDATA[ Ashley Thieme ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/3AWovHjApwuNrSGRS6WBcL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Ashley Thieme is a staff writer on the Reviews team at Tom’s Guide where she tests out the latest tech so you can know what’s going to be worth your time and money. She has a master’s degree in Magazine Journalism and a bachelor’s degree in Journalism, Media and Sociology from Cardiff University. She has bylines in titles including Women’s Health UK, sharing the latest health and wellness news stories, and Virgin Radio UK, providing the latest entertainment news and working on celebrity interviews. She has experience reporting on a variety of topics including music, literature, motorsport, entertainment and health. In previously published work, she has reviewed live music events, books, and wellness products. She values the importance of tech enhancing your life rather than taking over, and as a music fanatic, she is always looking for the best way to listen to new music releases. Discovering the top audio equipment that enhances sound quality and provides optimum comfort is what Ashley does best. In her spare time, Ashley enjoys hitting her reading goals on Goodreads by getting into the latest novels with a cup of tea as well as getting out in the Welsh mountains for a good hike on the weekend.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/mpYmpr4wAMUdFoxsr362Cg-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Tribit StormBox Micro 3]]></media:description>                                                            <media:text><![CDATA[Tribit StormBox Micro 3]]></media:text>
                                <media:title type="plain"><![CDATA[Tribit StormBox Micro 3]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/mpYmpr4wAMUdFoxsr362Cg-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>As a reviews writer here at Tom's Guide, I get my hands on a lot of the latest tech, including some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a>. And funnily enough, some of my favorites have been some of the cheapest offerings. </p><p>Tribit is a budget audio brand, but its Bluetooth speakers always turn out to be some of my favorites. I recently tested the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit StormBox Micro 3,</a> and it totally earns it's 4.5 star rating. </p><p>The speaker is only $64, and can often be found on sale for even less. Like right now, it's down to <a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S">$49 at Amazon U.S</a>.</p><div class="product"><a data-dimension112="f05d8fe3-c877-4865-9d92-522d03a24fab" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="RS3PDefgkukwjHkV9sCy9M" name="tribit-stormbox-micro-3-portable-bluetoo-9e0eed43-f6d4-445f-be8d-6cac88998b28.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/RS3PDefgkukwjHkV9sCy9M.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Tribit StormBox Micro 3 offers an IP68 rating, a companion app for EQ customization, and simple controls that make the speaker a breeze to use. It has 24 hours of battery life and can even be used as a portable charger. But most importantly, the audio performance really is excellent.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="f05d8fe3-c877-4865-9d92-522d03a24fab" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$49">View Deal</a></p></div><h2 id="excellent-audio-performance">Excellent audio performance</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AdztFXRYm2qRnPoAbredeU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/AdztFXRYm2qRnPoAbredeU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Of course, sound is the most important thing to consider when buying a new Bluetooth speaker, and the Tribit StormBox Micro 3 nails it.  For such a small speaker, it creates a big sound. </p><p>And when the BassX setting is deployed, you have home-filling sound. I love using this feature in the house or just in the backyard while taking care of some chores. You can also switch up the EQ settings in the Tribit companion app, but the BassX button on the speaker makes switching up the sound incredibly easy, and it suits most music genres. </p><h2 id="top-choice-for-durability">Top choice for durability</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5C4rm2BEgQpXe64xtKfNaU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/5C4rm2BEgQpXe64xtKfNaU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>With a durability rating of IP68, this speaker can take on anything. With this rating, the speaker can actually withstand water submersion and is completely dust-tight. </p><p>You don't need to worry about dropping it, either, as it's been rated to survive falls from as much as four feet. I'm the world's worst butterfinger, so I did end up dropping the speaker a few times, and it was completely fine.  </p><h2 id="all-day-battery">All-day battery</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9JFp65GWDw98dPHu7u9yXU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9JFp65GWDw98dPHu7u9yXU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The 24-hour battery life is one of the best things about this speaker. It's double the battery life of some competitors, and it doesn't suffer too much at high volumes. </p><p>If using the BassX settings, the battery drains faster, but I was still able to get a good 18 hours, which is very impressive. </p><h2 id="not-exactly-micro">Not exactly "micro"</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dLJQsthExUW6jmoyP5wHVU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/dLJQsthExUW6jmoyP5wHVU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Though the StormBox Micro 3 is small, Tribit actually makes smaller speakers than this. The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a>, for example, is a lot smaller, and I find it far easier to carry around with me. </p><p>It's also surprisingly heavy, but that's down to the speaker being magnetic. This feature is incredibly convenient, and in some ways makes it worth the extra weight. </p><p>To find out more about the Bluetooth speaker and what it can do, you can read my full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit StromBox Micro 3 review</a>.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review#section-tribit-stormbox-micro-3-review-sound-quality"><strong>Tribit has done it again — the brand-new StormBox Micro 3 rivals JBL for under $70</strong></a></li><li><a href="https://www.tomsguide.com/audio/you-can-take-a-portable-bluetooth-speaker-with-you-camping-but-heres-why-its-not-the-best-idea-to-use-it-on-your-hike"><strong>You can take a portable Bluetooth speaker with you camping, but here's why it's not the best idea to use it on your hike</strong></a></li><li><a href="https://www.tomsguide.com/ai/ever-wonder-why-chatgpt-is-free-the-answer-is-far-more-calculated-than-you-might-think"><strong>Ever wonder why ChatGPT is free? The answer is far more calculated than you might think</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just tested KEF’s first soundbar — it’s $2,500 and one of the best home cinema products I’ve ever used ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The KEF XIO is one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> I’ve ever had the pleasure of using. Review over, that’s me done for the day. I jest, I jest — but seriously, if you want the best single-product home cinema experience possible, you’ll want to know about the KEF XIO. </p><p>At 23 pounds, almost 50 inches long, and 6.5 inches deep, this is a pretty hefty soundbar. You’ll ideally want at least a 55-inch TV (if not bigger) and a large entertainment unit or TV stand to house it. But if you’ve got those things, a 4K Blu-ray player to take full advantage of the immense sound, and $2,500? Then, yes, you will not be disappointed by the KEF XIO. </p><p>I knocked off a star for some app connectivity issues, which I’ll go into later on, but if you’re all about getting the best home sound without fiddling around with playback modes,  you’ll want to find out more in this KEF XIO review.</p><h2 class="article-body__section" id="section-kef-xio-review-specs"><span>KEF XIO review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/KEF-Dolby-Atmos-5-1-2-Soundbar/dp/B0FHWTQ44W" target="_blank" rel="nofollow">$2,499</a> / <a href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow">£1,999</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI eARC, Ethernet, optical, Wi-Fi (for AirPlay, KEF Connect), Bluetooth</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>23 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>47.6 x 2.8 x 6.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, gray</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Built-in and sub out</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>5.1.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-kef-xio-review-price-availability"><span>KEF XIO review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HWiCWSrVVpXkcn6wnGbPcE" name="KEF XIO 10.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/HWiCWSrVVpXkcn6wnGbPcE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO is a whopping <a href="https://www.amazon.com/KEF-Dolby-Atmos-5-1-2-Soundbar/dp/B0FHWTQ44W" target="_blank" rel="nofollow">$2,499 from Amazon U.S.</a> and <a href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow">£1,999 from Richer Sounds</a> and <a href="https://www.sevenoakssoundandvision.co.uk/p-69434-kef-xio-soundbar.aspx" target="_blank" rel="nofollow">Sevenoaks</a> in the U.K..</p><p>This is a premium soundbar with premium sound to match. If $2,499 is a little out of your price range but you still want a “best of the best” home cinema setup, I’d recommend checking out the <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300x-review">JBL Bar 1300X</a> ($1,799), or the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> ($899). </p><p>I’ve also tested the <a href="https://www.tomsguide.com/audio/soundbars/yamaha-true-x-surround-90a-review">Yamaha True X Surround 90A</a> ($3,499), which is the best Dolby Atmos soundbar I’ve ever reviewed. However, it’s an eye-watering $1,000 more expensive than the KEF XIO, so I’d recommend the XIO over the True X Surround 90A. </p><h2 class="article-body__section" id="section-kef-xio-review-design"><span>KEF XIO review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xyX3GxJw7T547tu95wz2o3" name="KEF XIO 9.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/xyX3GxJw7T547tu95wz2o3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO is a long, heavy soundbar. You’ll need a 55” TV at minimum, or else it'll look at little odd having such a huge soundbar underneath. I tested it with a 55” Amazon Ember QLED, and it was <em>almost</em> too small. I’d recommend a 60-incher and up. </p><p>As you might expect for $2,499, the KEF XIO is fantastically well-made, with a minimalistic yet modern appearance. There’s a nice mesh panel that diffuses sibilant treble and looks stylish, and a control panel on the top of the soundbar. It’s available in black or gray — as you can see from the images, I tested gray — and I have no reservations about recommending it based on looks alone.</p><p>The KEF XIO has a subwoofer out (alongside a built-in sub), which you can theoretically connect to any compatible subwoofer. KEF offers the KW2 wireless adaptor, which can wirelessly connect any KEF subwoofer to the XIO and the Connect app. You can also get the <a href="https://www.amazon.com/KEF-KC92-Subwoofer-White-Gloss/dp/B0CX2VJ685" target="_blank" rel="nofollow">KEF KC92</a> ($2,499) which connects to KEF’s smart system without a separate adaptor. </p><h2 id="controls-13">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zLsXnpprAkbhgDEsSWaXp3" name="KEF XIO 5.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/zLsXnpprAkbhgDEsSWaXp3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The remote is much bigger than Samsung, Bose, and Sony’s soundbar remotes. It’s also <em>much</em> more premium-looking than Bose’s remote. It’s not quite as innovative as Samsung’s new solar-powered remotes, but it still looks and feels great. </p><p>You can also use the KEF Connect app as a mobile remote (for certain inputs), which I’ll discuss in detail in the ‘App’ section below.</p><h2 id="features-6">Features</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Cvnoeiyxcthk95GfLGsQd4" name="KEF XIO 3.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/Cvnoeiyxcthk95GfLGsQd4.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As soon as I set up the XIO with the KEF Connect app — which was seamless and stress-free, phew — the soundbar automatically calibrated itself for my space. If you have particularly discerning ears, I’d recommend getting a soundbar with this calibration software built-in, like the JBL Bar 1300X, the KEF XIO, or the Klipsch Flexus Core 300. </p><p>After inputting room size and soundbar height information into the app, the soundbar adjusted itself using built-in microphones and a gyroscope. This is an essential feature if you’re after “perfect” sound quality. </p><p>The XIO offers a range of sound-related features, too. Of course, you’ve got the standard EQs: ‘Dialogue’, ‘Night’, ‘Movie’, and ‘Direct’. I pretty much only listened using ‘Movie’, as dialogue was already clear enough. As you’ll find out soon, the KEF XIO’s sound is astronomically good. I have nothing bad to say about the sound. </p><h2 class="article-body__section" id="section-kef-xio-review-app"><span>KEF XIO review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QeWUC7vaa2fatzedBk8ys3" name="KEF XIO 6.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/QeWUC7vaa2fatzedBk8ys3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO uses KEF Connect, the app also used for KEF speakers like the<a href="https://www.tomsguide.com/audio/speakers/i-just-tested-these-incredible-stereo-speakers-and-theyre-better-than-any-soundbar-ive-ever-tested"> LSX II</a> ($1,499). The app is easy enough to use, and connecting to the soundbar itself was instantaneous. Some apps are better than others, though, and I feel Samsung’s SmartThings app has more options and is easier to navigate.</p><p>Even so, there’s a ton of customization options in KEF Connect — as long as you’re listening in certain playback modes. Weirdly, customization is not available in TV mode, only Bluetooth and Wi-Fi. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CqZ3SoDDg3ar4Tf38uFCdR" name="kef xio screenshots" alt="three screenshots from the KEF connect app showing usability with the KEF XIO" src="https://cdn.mos.cms.futurecdn.net/CqZ3SoDDg3ar4Tf38uFCdR.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/CqZ3SoDDg3ar4Tf38uFCdR.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: KEF)</span></figcaption></figure><p>This is due to HDMI CEC incompatibility, but I connected the XIO to HDMI eARC in two different TVs and had the same issue. I adjusted both TV settings, reset both TV CEC settings, but still, I was unable to control the soundbar via my phone while watching TV. </p><p>This is a major issue for such a premium soundbar — and the main thing holding it back from being a 5-star product. </p><h2 class="article-body__section" id="section-kef-xio-review-sound-quality"><span>KEF XIO review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bhdwbfzdph6ihP2ibBU2nE" name="KEF XIO 4.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/bhdwbfzdph6ihP2ibBU2nE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I test every soundbar with “Top Gun: Maverick” on 4K UHD Blu-Ray (and then two extras), TV shows on <a href="https://www.tomsguide.com/reviews/netflix">Netflix</a>, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-6">Movies</h2><p>The KEF XIO is one of the best soundbars I’ve ever used for movies. I have nothing bad to say about the sound quality here. </p><p>During "Top Gun: Maverick", I listened with ‘Movie’ and ‘Dialogue’ EQ, but quickly switched back to ‘Movie’. The soundscape was incredibly wide during the first scene; I could feel the planes flying across the room, I could hear the high-frequency diegetic sound effects like whipping ropes and clinking metals without straining. The groovy bass of ‘Danger Zone’ pounded through the soundbar but let the softer sound effects shine. </p><p>Dialogue was clear, even without ‘Dialogue’ mode, even during the radio scenes. Flying scenes sounded outrageously good; the bass rumbled in my chest when the jets took off. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FRCh9hHdMCgF8FVXY5ZKj3" name="KEF XIO 11.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/FRCh9hHdMCgF8FVXY5ZKj3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Dolby Atmos performance is utterly fantastic, especially in "Top Gun: Maverick". Every single flying scene had unbeatable height and width, and the planes seemed to fly over my head. I was incredibly impressed with this. For $2,499, you’d hope so — but I’m pleased to report that the soundbar is worth it. </p><p>I also watched “How to Train Your Dragon (2025)” and “The Running Man (2025)”. During “How to Train Your Dragon”, the soft footsteps across the mossy ground were audible over the choral OST. In one scene, I could hear diegetic murmured chatter from behind me, while the central characters’ conversation was front and center on the mix. The flying scenes were particularly impressive, with the brass-led soundtrack booming through the soundbar without overpowering the on-screen sounds. </p><p>In “The Running Man”, I could hear the static hum of the retro TVs and on-screen lights. High-frequency diegetic sound effects like typing, opening doors, and pinging bullets carried across the soundstage well. </p><p>Although this movie didn’t have the opportunity for <em>super</em> impressive Dolby Atmos — unlike "Top Gun: Maverick" — I was still immersed in the sound. </p><h2 id="tv-6">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WwrCzgc39Ymnhi4RNijjS4" name="KEF XIO 1.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/WwrCzgc39Ymnhi4RNijjS4.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test standard mix, non-Atmos-mixed sound, I watched TV on Netflix and ITVX (a U.K. TV channel like BBC). </p><p>I watched “Yellowjackets” on ITVX, a horror/thriller show. The soundtrack has an eerie, string-heavy, bass-heavy production, and I never struggled to hear dialogue over the OST. All the string instruments were clear, with audible vibration in the notes, and a controlled yet propulsive bass response. </p><p>Even in basic 2.0 configuration (ITVX’s output), the show sounded fantastic. </p><p>I also watched “Yellowjackets” on Paramount+ on 5.1, and the difference was astounding. On Paramount+, the dialogue was richer and warmer, and the high-frequency sound effects were clearer. </p><p>While the KEF XIO performs well even with 2.0 channel output, it really shines with 5.1 and above (obviously). </p><h2 id="music-6">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ELHAbt4ZdScUC4GfGzuijE" name="KEF XIO 7.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/ELHAbt4ZdScUC4GfGzuijE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I would best describe music performance as ‘okay’. No soundbar really excels with music — it’s not what they’re designed to do — but the KEF XIO isn’t particularly bad or good. </p><p>‘Genesis’ by Grimes had great treble performance; the synth notes tinkled around the room like a glitch-pop fairy powered by Ableton and hallucinogens. Although I longed for a bit more <em>oomph</em> in the bass, I would have no qualms about listening to music like this while cleaning or cooking. </p><p>After, I switched genre to rock and played ‘Unravelling’ by Muse. Again, I wanted a touch more bass, but the vocals and synth arpeggios sounded divine. You won’t be filling any floors with music like this, but, again — it’s a soundbar. </p><p>For blockbuster movies, and even standard-defintion TV shows, the KEF XIO is a sure-fire winner. It sounds sublime. </p><h2 class="article-body__section" id="section-kef-xio-review-verdict"><span>KEF XIO review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EJyg3chu6kb86W9joe7qb3" name="KEF XIO 8.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/EJyg3chu6kb86W9joe7qb3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>If you’re willing to overlook potential app connectivity issues — I personally never lean towards downloading ‘smart’ apps, so this is a non-issue for me — then you won’t be disappointed with the KEF XIO. This is a mighty, mighty soundbar. It’d be a great centerpiece of your home cinema setup. </p><p>The soundbar has enviable width and height, with unbeatable dialogue clarity and powerful bass that doesn’t overpower subtle high-frequency sound effects. The XIO is everything I want in a soundbar, and I have zero reservations about recommend it. </p><p>If you can afford it, the XIO is a formidable sonic beast that will mark its territory fearlessly. Can you get similar enough performance for less? Sure. Will those alternatives make you go “Ooh, that sounds <em>so</em> good”? Probably not. It’s gotta be the KEF XIO. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/kef-xio-review</link>
                                                                            <description>
                            <![CDATA[ KEF XIO is a soundbar with big ambitions and a big price tag. At $2,500, this is only for the serious home cinema enthusiasts, but it won’t disappoint you. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">i7yMLY8ifxMLWsRDL3iWwh</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/WwrCzgc39Ymnhi4RNijjS4-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 12 Jun 2026 12:06:42 +0000</pubDate>                                                                                                                                <updated>Tue, 16 Jun 2026 16:05:28 +0000</updated>
                                                                                                                                            <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/WwrCzgc39Ymnhi4RNijjS4-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the KEF XIO soundbar in gray photographed with a TV and the Tom&#039;s Guide blue background]]></media:description>                                                            <media:text><![CDATA[the KEF XIO soundbar in gray photographed with a TV and the Tom&#039;s Guide blue background]]></media:text>
                                <media:title type="plain"><![CDATA[the KEF XIO soundbar in gray photographed with a TV and the Tom&#039;s Guide blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/WwrCzgc39Ymnhi4RNijjS4-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The KEF XIO is one of the <a href="https://www.tomsguide.com/us/best-soundbar-speakers,review-2004.html">best soundbars</a> I’ve ever had the pleasure of using. Review over, that’s me done for the day. I jest, I jest — but seriously, if you want the best single-product home cinema experience possible, you’ll want to know about the KEF XIO. </p><p>At 23 pounds, almost 50 inches long, and 6.5 inches deep, this is a pretty hefty soundbar. You’ll ideally want at least a 55-inch TV (if not bigger) and a large entertainment unit or TV stand to house it. But if you’ve got those things, a 4K Blu-ray player to take full advantage of the immense sound, and $2,500? Then, yes, you will not be disappointed by the KEF XIO. </p><p>I knocked off a star for some app connectivity issues, which I’ll go into later on, but if you’re all about getting the best home sound without fiddling around with playback modes,  you’ll want to find out more in this KEF XIO review.</p><h2 class="article-body__section" id="section-kef-xio-review-specs"><span>KEF XIO review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/KEF-Dolby-Atmos-5-1-2-Soundbar/dp/B0FHWTQ44W" target="_blank" rel="nofollow">$2,499</a> / <a href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow">£1,999</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>HDMI eARC, Ethernet, optical, Wi-Fi (for AirPlay, KEF Connect), Bluetooth</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>23 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>47.6 x 2.8 x 6.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, gray</p></td></tr><tr><td class="firstcol " ><p><strong>Subwoofer</strong></p></td><td  ><p>Built-in and sub out</p></td></tr><tr><td class="firstcol " ><p><strong>Dolby Atmos</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Channel</strong></p></td><td  ><p>5.1.2</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-kef-xio-review-price-availability"><span>KEF XIO review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HWiCWSrVVpXkcn6wnGbPcE" name="KEF XIO 10.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/HWiCWSrVVpXkcn6wnGbPcE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO is a whopping <a href="https://www.amazon.com/KEF-Dolby-Atmos-5-1-2-Soundbar/dp/B0FHWTQ44W" target="_blank" rel="nofollow">$2,499 from Amazon U.S.</a> and <a href="https://www.richersounds.com/kef-xio-slate-black" target="_blank" rel="nofollow">£1,999 from Richer Sounds</a> and <a href="https://www.sevenoakssoundandvision.co.uk/p-69434-kef-xio-soundbar.aspx" target="_blank" rel="nofollow">Sevenoaks</a> in the U.K..</p><p>This is a premium soundbar with premium sound to match. If $2,499 is a little out of your price range but you still want a “best of the best” home cinema setup, I’d recommend checking out the <a href="https://www.tomsguide.com/audio/soundbars/jbl-bar-1300x-review">JBL Bar 1300X</a> ($1,799), or the <a href="https://www.tomsguide.com/audio/soundbars/samsung-qs750f-review">Samsung QS750F</a> ($899). </p><p>I’ve also tested the <a href="https://www.tomsguide.com/audio/soundbars/yamaha-true-x-surround-90a-review">Yamaha True X Surround 90A</a> ($3,499), which is the best Dolby Atmos soundbar I’ve ever reviewed. However, it’s an eye-watering $1,000 more expensive than the KEF XIO, so I’d recommend the XIO over the True X Surround 90A. </p><h2 class="article-body__section" id="section-kef-xio-review-design"><span>KEF XIO review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xyX3GxJw7T547tu95wz2o3" name="KEF XIO 9.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/xyX3GxJw7T547tu95wz2o3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO is a long, heavy soundbar. You’ll need a 55” TV at minimum, or else it'll look at little odd having such a huge soundbar underneath. I tested it with a 55” Amazon Ember QLED, and it was <em>almost</em> too small. I’d recommend a 60-incher and up. </p><p>As you might expect for $2,499, the KEF XIO is fantastically well-made, with a minimalistic yet modern appearance. There’s a nice mesh panel that diffuses sibilant treble and looks stylish, and a control panel on the top of the soundbar. It’s available in black or gray — as you can see from the images, I tested gray — and I have no reservations about recommending it based on looks alone.</p><p>The KEF XIO has a subwoofer out (alongside a built-in sub), which you can theoretically connect to any compatible subwoofer. KEF offers the KW2 wireless adaptor, which can wirelessly connect any KEF subwoofer to the XIO and the Connect app. You can also get the <a href="https://www.amazon.com/KEF-KC92-Subwoofer-White-Gloss/dp/B0CX2VJ685" target="_blank" rel="nofollow">KEF KC92</a> ($2,499) which connects to KEF’s smart system without a separate adaptor. </p><h2 id="controls-13">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zLsXnpprAkbhgDEsSWaXp3" name="KEF XIO 5.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/zLsXnpprAkbhgDEsSWaXp3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The remote is much bigger than Samsung, Bose, and Sony’s soundbar remotes. It’s also <em>much</em> more premium-looking than Bose’s remote. It’s not quite as innovative as Samsung’s new solar-powered remotes, but it still looks and feels great. </p><p>You can also use the KEF Connect app as a mobile remote (for certain inputs), which I’ll discuss in detail in the ‘App’ section below.</p><h2 id="features-6">Features</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Cvnoeiyxcthk95GfLGsQd4" name="KEF XIO 3.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/Cvnoeiyxcthk95GfLGsQd4.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As soon as I set up the XIO with the KEF Connect app — which was seamless and stress-free, phew — the soundbar automatically calibrated itself for my space. If you have particularly discerning ears, I’d recommend getting a soundbar with this calibration software built-in, like the JBL Bar 1300X, the KEF XIO, or the Klipsch Flexus Core 300. </p><p>After inputting room size and soundbar height information into the app, the soundbar adjusted itself using built-in microphones and a gyroscope. This is an essential feature if you’re after “perfect” sound quality. </p><p>The XIO offers a range of sound-related features, too. Of course, you’ve got the standard EQs: ‘Dialogue’, ‘Night’, ‘Movie’, and ‘Direct’. I pretty much only listened using ‘Movie’, as dialogue was already clear enough. As you’ll find out soon, the KEF XIO’s sound is astronomically good. I have nothing bad to say about the sound. </p><h2 class="article-body__section" id="section-kef-xio-review-app"><span>KEF XIO review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QeWUC7vaa2fatzedBk8ys3" name="KEF XIO 6.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/QeWUC7vaa2fatzedBk8ys3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The KEF XIO uses KEF Connect, the app also used for KEF speakers like the<a href="https://www.tomsguide.com/audio/speakers/i-just-tested-these-incredible-stereo-speakers-and-theyre-better-than-any-soundbar-ive-ever-tested"> LSX II</a> ($1,499). The app is easy enough to use, and connecting to the soundbar itself was instantaneous. Some apps are better than others, though, and I feel Samsung’s SmartThings app has more options and is easier to navigate.</p><p>Even so, there’s a ton of customization options in KEF Connect — as long as you’re listening in certain playback modes. Weirdly, customization is not available in TV mode, only Bluetooth and Wi-Fi. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CqZ3SoDDg3ar4Tf38uFCdR" name="kef xio screenshots" alt="three screenshots from the KEF connect app showing usability with the KEF XIO" src="https://cdn.mos.cms.futurecdn.net/CqZ3SoDDg3ar4Tf38uFCdR.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/CqZ3SoDDg3ar4Tf38uFCdR.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: KEF)</span></figcaption></figure><p>This is due to HDMI CEC incompatibility, but I connected the XIO to HDMI eARC in two different TVs and had the same issue. I adjusted both TV settings, reset both TV CEC settings, but still, I was unable to control the soundbar via my phone while watching TV. </p><p>This is a major issue for such a premium soundbar — and the main thing holding it back from being a 5-star product. </p><h2 class="article-body__section" id="section-kef-xio-review-sound-quality"><span>KEF XIO review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bhdwbfzdph6ihP2ibBU2nE" name="KEF XIO 4.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/bhdwbfzdph6ihP2ibBU2nE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I test every soundbar with “Top Gun: Maverick” on 4K UHD Blu-Ray (and then two extras), TV shows on <a href="https://www.tomsguide.com/reviews/netflix">Netflix</a>, and music on <a href="https://www.whathifi.com/qobuz/review">Qobuz</a> via AirPlay.</p><h2 id="movies-6">Movies</h2><p>The KEF XIO is one of the best soundbars I’ve ever used for movies. I have nothing bad to say about the sound quality here. </p><p>During "Top Gun: Maverick", I listened with ‘Movie’ and ‘Dialogue’ EQ, but quickly switched back to ‘Movie’. The soundscape was incredibly wide during the first scene; I could feel the planes flying across the room, I could hear the high-frequency diegetic sound effects like whipping ropes and clinking metals without straining. The groovy bass of ‘Danger Zone’ pounded through the soundbar but let the softer sound effects shine. </p><p>Dialogue was clear, even without ‘Dialogue’ mode, even during the radio scenes. Flying scenes sounded outrageously good; the bass rumbled in my chest when the jets took off. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FRCh9hHdMCgF8FVXY5ZKj3" name="KEF XIO 11.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/FRCh9hHdMCgF8FVXY5ZKj3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Dolby Atmos performance is utterly fantastic, especially in "Top Gun: Maverick". Every single flying scene had unbeatable height and width, and the planes seemed to fly over my head. I was incredibly impressed with this. For $2,499, you’d hope so — but I’m pleased to report that the soundbar is worth it. </p><p>I also watched “How to Train Your Dragon (2025)” and “The Running Man (2025)”. During “How to Train Your Dragon”, the soft footsteps across the mossy ground were audible over the choral OST. In one scene, I could hear diegetic murmured chatter from behind me, while the central characters’ conversation was front and center on the mix. The flying scenes were particularly impressive, with the brass-led soundtrack booming through the soundbar without overpowering the on-screen sounds. </p><p>In “The Running Man”, I could hear the static hum of the retro TVs and on-screen lights. High-frequency diegetic sound effects like typing, opening doors, and pinging bullets carried across the soundstage well. </p><p>Although this movie didn’t have the opportunity for <em>super</em> impressive Dolby Atmos — unlike "Top Gun: Maverick" — I was still immersed in the sound. </p><h2 id="tv-6">TV</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WwrCzgc39Ymnhi4RNijjS4" name="KEF XIO 1.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/WwrCzgc39Ymnhi4RNijjS4.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To test standard mix, non-Atmos-mixed sound, I watched TV on Netflix and ITVX (a U.K. TV channel like BBC). </p><p>I watched “Yellowjackets” on ITVX, a horror/thriller show. The soundtrack has an eerie, string-heavy, bass-heavy production, and I never struggled to hear dialogue over the OST. All the string instruments were clear, with audible vibration in the notes, and a controlled yet propulsive bass response. </p><p>Even in basic 2.0 configuration (ITVX’s output), the show sounded fantastic. </p><p>I also watched “Yellowjackets” on Paramount+ on 5.1, and the difference was astounding. On Paramount+, the dialogue was richer and warmer, and the high-frequency sound effects were clearer. </p><p>While the KEF XIO performs well even with 2.0 channel output, it really shines with 5.1 and above (obviously). </p><h2 id="music-6">Music</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ELHAbt4ZdScUC4GfGzuijE" name="KEF XIO 7.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/ELHAbt4ZdScUC4GfGzuijE.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I would best describe music performance as ‘okay’. No soundbar really excels with music — it’s not what they’re designed to do — but the KEF XIO isn’t particularly bad or good. </p><p>‘Genesis’ by Grimes had great treble performance; the synth notes tinkled around the room like a glitch-pop fairy powered by Ableton and hallucinogens. Although I longed for a bit more <em>oomph</em> in the bass, I would have no qualms about listening to music like this while cleaning or cooking. </p><p>After, I switched genre to rock and played ‘Unravelling’ by Muse. Again, I wanted a touch more bass, but the vocals and synth arpeggios sounded divine. You won’t be filling any floors with music like this, but, again — it’s a soundbar. </p><p>For blockbuster movies, and even standard-defintion TV shows, the KEF XIO is a sure-fire winner. It sounds sublime. </p><h2 class="article-body__section" id="section-kef-xio-review-verdict"><span>KEF XIO review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EJyg3chu6kb86W9joe7qb3" name="KEF XIO 8.JPG" alt="the KEF XIO soundbar in gray photographed with a TV and the Tom's Guide blue background" src="https://cdn.mos.cms.futurecdn.net/EJyg3chu6kb86W9joe7qb3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>If you’re willing to overlook potential app connectivity issues — I personally never lean towards downloading ‘smart’ apps, so this is a non-issue for me — then you won’t be disappointed with the KEF XIO. This is a mighty, mighty soundbar. It’d be a great centerpiece of your home cinema setup. </p><p>The soundbar has enviable width and height, with unbeatable dialogue clarity and powerful bass that doesn’t overpower subtle high-frequency sound effects. The XIO is everything I want in a soundbar, and I have zero reservations about recommend it. </p><p>If you can afford it, the XIO is a formidable sonic beast that will mark its territory fearlessly. Can you get similar enough performance for less? Sure. Will those alternatives make you go “Ooh, that sounds <em>so</em> good”? Probably not. It’s gotta be the KEF XIO. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Tribit has done it again — the brand-new StormBox Micro 3 rivals JBL for under $70 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’m a big fan of Tribit’s speakers, and the Tribit Strombox Micro 3 has joined the ranks as one of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> I’ve tested. It has seriously excellent audio performance for a variety of genres, EQ customization, and simple controls. On top of that, it’s waterproof, dustproof, and dropproof. The 24-hour battery life is the cherry on top, and it can even be used as a portable charger for your phone in a pinch. </p><p>The only downside is that some of the EQ presets, such as ‘Rock’, compress the sound, and you lose the full effect of the wide soundstage. </p><p>Some people may not love the basic appearance, but I don’t mind it as I think it fits in any situation. </p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-specs"><span>Tribit StormBox Micro 3 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">$64</a> / <a href="https://www.amazon.co.uk/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">£62</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>IP68</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 6.0</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>True Wireless Stereo (TWS)</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>147.6 feet</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>24 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>4.4 x 4.3 x 1.8 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>0.73 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Blue, Gray White</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-price-availability"><span>Tribit StormBox Micro 3 review: Price & availability</span></h2><p>The Tribit StormBox Micro 3 is available to purchase from Amazon for <a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">$64</a> / <a href="https://www.amazon.co.uk/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">£62</a>. It comes in three different colors, including black, blue and grayish white. This is the third iteration of Tribit’s StormBox and is only slightly more expensive than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-2-review">Tribit StormBox Micro 2</a>, which costs $59. </p><p>The StormBox Micro 3 is also more expensive than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a>, which has become my most reliable portable speaker. The PocketGo costs $34 and has huge sound without a huge price tag. </p><p>While the StormBox Micro 2 is a cheap offering in the Bluetooth speaker world, there are even cheaper options: The cheapest Bluetooth speaker I’ve tested is the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tozo-pe1-review#section-tozo-pe1-review-price-availability">Tozo PE1</a>, which costs $29, but it’s worth spending more for the better bass performance offered by the StormBox Micro 3.</p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-design"><span>Tribit StormBox Micro 3 review: Design</span></h2><p>At 4.4 x 4.3 x 1.8 inches and 0.7 pounds, I think the Tribit StormBox Micro 3 is a little big to call itself ‘micro,’ but it’s still small enough to fit nicely into your backpack. It also has a flexible strap on the back so it can be attached to anything with a diameter of under 1.1 inches, such as handlebars on a bike. </p><p>It was a lot heavier than I expected, but because it has a magnet inside, it can be mounted to any magnetic surface, which is probably the best way to carry it around.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="C3Gi3PUm4SKxCjN3sJbNuU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/C3Gi3PUm4SKxCjN3sJbNuU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I tested the black color, which has white buttons and a mesh overlay in front of the speaker. It looks a lot like the previous StormBox Micro speaker, but with more rounded edges, which softens the rugged look. </p><p>The speaker has a durability rating of IP68, meaning it is completely waterproof and dustproof. It’s also been military drop-tested up to a height of four feet. </p><h3 id="controls-14">Controls</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9Hk48wSZACfyKtv8ozhNkU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9Hk48wSZACfyKtv8ozhNkU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The controls on the Tribit StormBox Micro 3 are very simple, and since I’ve used Tribit speakers in the past, including the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review">Tribit StormBox Lava</a> ($129) and the Tribit PocketGo, the controls felt very natural to me. </p><p>On the side of the speaker, you'll find a power button, a Bluetooth connection button, a BassX EQ preset button, and a TWS (True Wireless Stereo) button for linking speakers.</p><p>On top, there are larger buttons for volume, play, and pause. I like that these buttons are big and easily accessible, as they're the ones you'll reach for most.</p><h3 id="connectivity-10">Connectivity</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QWtdRLRBHi8vkzMgq8CF5V" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/QWtdRLRBHi8vkzMgq8CF5V.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Tribit StormBox Micro 3 uses Bluetooth 6.0. The speaker has a connection range of nearly 150 feet, so I was able to walk away from the speaker all the way into my garden without the connection breaking up. </p><p>The speaker also has TWS (True Wireless Stereo, not to be confused with True Wireless), which means you can connect multiple speakers together at the same time to create a stereo setup. </p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-sound-quality"><span>Tribit StormBox Micro 3 review: Sound quality</span></h2><p>For such a small speaker, the Tribit StormBox Micro 3 creates a big sound.  I think the speaker’s signature sound with the BassX setting already sounds great, but I got the best sound when using a custom EQ setting. </p><p>To test bass performance, I listened to <em>Tonight (I Wish I Was Your Boy)</em> by The 1975 using the BassX EQ. The punchy bass drives this song, and the speaker did a good job of keeping it controlled. It sounded clean while not overpowering the other components of the song, which still sounded sharp and balanced. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9WyziVp5FTGpjSMkxAbUvU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9WyziVp5FTGpjSMkxAbUvU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I then turned to the mid-performance of the speaker by listening to <em>Ok Love You Bye</em> by Olivia Dean. This track has a lot of layers that played perfectly with Tribit's signature EQ. The woodwind and brass instruments were very prominent throughout, and the vocals remained strong. I could hear how broad the soundstage is when listening to this song, and I was impressed by the performance. </p><p>Finally, I tested out the treble by listening to <em>In Bloom</em> by Neck Deep. This track is very bright with a complex percussion rhythm from the drums. For this track, I made a U-shaped  Custom EQ.  There were no issues with muddy sounds, and although there’s a lot going on, the instrument separation was excellent.</p><p>Overall, I was impressed with the way it played my favorite songs, but I wouldn’t recommend using the EQ presets, as some caused a lot of compression.</p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-app"><span>Tribit StormBox Micro 3 review: App</span></h2><p>The StormBox Micro 3 can be used with the Tribit app, which is available to download for free. </p><p>In the app, you’ll find six EQ presets you can choose from, or you can make your own custom EQ settings. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="HqSYnpzhSxC7pM3iP6eKtX" name="Tribit StormBox Micro 3 app" alt="Screenshots from the Tribit app showing the EQ controls of the Tribit StormBox Micro 3" src="https://cdn.mos.cms.futurecdn.net/HqSYnpzhSxC7pM3iP6eKtX.jpg" mos="" align="middle" fullscreen="" width="1510" height="850" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tribit)</span></figcaption></figure><p>The EQ uses nine bands, so there’s a lot of scope to make changes to the sound to fit your personal preference. </p><p>You can also set a sleep timer for automatic shut-off of the speaker, which is pretty useful, as well as see the remaining battery on the speaker. </p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-battery-life"><span>Tribit StormBox Micro 3 review: Battery life</span></h2><p>Speaking of battery life, the Tribit StormBox Micro 3 has a rated battery life of 24 hours when used at half volume. This is plenty loud enough for casual listening, but if you’re using the speaker outdoors and need to listen a little louder, the battery will then drain faster. </p><p>Still, 24 hours is plenty of battery, and four more hours than you get with the Tribit PocketGo. This battery life is far superior to the JBL Go 5, which only has 10 hours. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9JFp65GWDw98dPHu7u9yXU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9JFp65GWDw98dPHu7u9yXU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Should you find yourself out of juice and in need of a quick boost, just 15 minutes of charging time with the included USB-C cable provides three hours of playback while a full charge takes around two hours. </p><p>Just like the Tribit StormBox Lava, the Micro 3 has reverse charging, which means you can use it as a portable charger in a pinch if you need to charge your phone.</p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-verdict"><span>Tribit StormBox Micro 3 review: Verdict</span></h2><p>Tribit has managed to make me love a budget speaker yet again. It's waterproof, dustproof, and drop-proof, making it an ideal option for anyone who wants to use the speaker outdoors. </p><p>Its companion app can be used for EQ customization, and the simple controls make it a breeze to use even without your phone nearby. The 24 hours of battery life mean you can listen all day, and the speaker can even be used as a portable charger in a pinch. But most importantly, the audio performance really is <em>excellent</em>.</p><p>For something slightly lighter, you could consider the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-clip-5-review">JBL Clip 5</a> for $79. This speaker also has a more sturdy clip at the top, rather than a flexible strap. The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> also gives beautiful sound for $54. The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ue-miniroll-review-tiny-but-mighty#section-ue-miniroll-review">UltimateEars Miniroll</a> costs $79 and also has a strap attachment at the back. This speaker is even lighter but is around the same size as the StormBox Micro 3. </p><p>For its price, however, the StormBox Micro 3 is tough to beat.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review</link>
                                                                            <description>
                            <![CDATA[ The Tribit StormBox Micro 3 proves that you don’t need to spend a lot of cash to get a durable speaker with awesome sound. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">GixpRa8FQxJtpdg6BjjXKc</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/ZdvddFbSvYJEvrGg6Ske2V-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Wed, 10 Jun 2026 06:15:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ ashley.thieme@futurenet.com (Ashley Thieme) ]]></author>                    <dc:creator><![CDATA[ Ashley Thieme ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/3AWovHjApwuNrSGRS6WBcL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Ashley Thieme is a staff writer on the Reviews team at Tom’s Guide where she tests out the latest tech so you can know what’s going to be worth your time and money. She has a master’s degree in Magazine Journalism and a bachelor’s degree in Journalism, Media and Sociology from Cardiff University. She has bylines in titles including Women’s Health UK, sharing the latest health and wellness news stories, and Virgin Radio UK, providing the latest entertainment news and working on celebrity interviews. She has experience reporting on a variety of topics including music, literature, motorsport, entertainment and health. In previously published work, she has reviewed live music events, books, and wellness products. She values the importance of tech enhancing your life rather than taking over, and as a music fanatic, she is always looking for the best way to listen to new music releases. Discovering the top audio equipment that enhances sound quality and provides optimum comfort is what Ashley does best. In her spare time, Ashley enjoys hitting her reading goals on Goodreads by getting into the latest novels with a cup of tea as well as getting out in the Welsh mountains for a good hike on the weekend.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/ZdvddFbSvYJEvrGg6Ske2V-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Tribit StormBox Micro 3 in black photographed in front of a blue background]]></media:description>                                                            <media:text><![CDATA[Tribit StormBox Micro 3 in black photographed in front of a blue background]]></media:text>
                                <media:title type="plain"><![CDATA[Tribit StormBox Micro 3 in black photographed in front of a blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/ZdvddFbSvYJEvrGg6Ske2V-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’m a big fan of Tribit’s speakers, and the Tribit Strombox Micro 3 has joined the ranks as one of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> I’ve tested. It has seriously excellent audio performance for a variety of genres, EQ customization, and simple controls. On top of that, it’s waterproof, dustproof, and dropproof. The 24-hour battery life is the cherry on top, and it can even be used as a portable charger for your phone in a pinch. </p><p>The only downside is that some of the EQ presets, such as ‘Rock’, compress the sound, and you lose the full effect of the wide soundstage. </p><p>Some people may not love the basic appearance, but I don’t mind it as I think it fits in any situation. </p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-specs"><span>Tribit StormBox Micro 3 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">$64</a> / <a href="https://www.amazon.co.uk/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">£62</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>IP68</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 6.0</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>True Wireless Stereo (TWS)</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>147.6 feet</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>24 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>4.4 x 4.3 x 1.8 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>0.73 pounds</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Blue, Gray White</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-price-availability"><span>Tribit StormBox Micro 3 review: Price & availability</span></h2><p>The Tribit StormBox Micro 3 is available to purchase from Amazon for <a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">$64</a> / <a href="https://www.amazon.co.uk/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow">£62</a>. It comes in three different colors, including black, blue and grayish white. This is the third iteration of Tribit’s StormBox and is only slightly more expensive than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-2-review">Tribit StormBox Micro 2</a>, which costs $59. </p><p>The StormBox Micro 3 is also more expensive than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a>, which has become my most reliable portable speaker. The PocketGo costs $34 and has huge sound without a huge price tag. </p><p>While the StormBox Micro 2 is a cheap offering in the Bluetooth speaker world, there are even cheaper options: The cheapest Bluetooth speaker I’ve tested is the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tozo-pe1-review#section-tozo-pe1-review-price-availability">Tozo PE1</a>, which costs $29, but it’s worth spending more for the better bass performance offered by the StormBox Micro 3.</p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-design"><span>Tribit StormBox Micro 3 review: Design</span></h2><p>At 4.4 x 4.3 x 1.8 inches and 0.7 pounds, I think the Tribit StormBox Micro 3 is a little big to call itself ‘micro,’ but it’s still small enough to fit nicely into your backpack. It also has a flexible strap on the back so it can be attached to anything with a diameter of under 1.1 inches, such as handlebars on a bike. </p><p>It was a lot heavier than I expected, but because it has a magnet inside, it can be mounted to any magnetic surface, which is probably the best way to carry it around.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="C3Gi3PUm4SKxCjN3sJbNuU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/C3Gi3PUm4SKxCjN3sJbNuU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I tested the black color, which has white buttons and a mesh overlay in front of the speaker. It looks a lot like the previous StormBox Micro speaker, but with more rounded edges, which softens the rugged look. </p><p>The speaker has a durability rating of IP68, meaning it is completely waterproof and dustproof. It’s also been military drop-tested up to a height of four feet. </p><h3 id="controls-14">Controls</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9Hk48wSZACfyKtv8ozhNkU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9Hk48wSZACfyKtv8ozhNkU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The controls on the Tribit StormBox Micro 3 are very simple, and since I’ve used Tribit speakers in the past, including the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review">Tribit StormBox Lava</a> ($129) and the Tribit PocketGo, the controls felt very natural to me. </p><p>On the side of the speaker, you'll find a power button, a Bluetooth connection button, a BassX EQ preset button, and a TWS (True Wireless Stereo) button for linking speakers.</p><p>On top, there are larger buttons for volume, play, and pause. I like that these buttons are big and easily accessible, as they're the ones you'll reach for most.</p><h3 id="connectivity-10">Connectivity</h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QWtdRLRBHi8vkzMgq8CF5V" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/QWtdRLRBHi8vkzMgq8CF5V.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Tribit StormBox Micro 3 uses Bluetooth 6.0. The speaker has a connection range of nearly 150 feet, so I was able to walk away from the speaker all the way into my garden without the connection breaking up. </p><p>The speaker also has TWS (True Wireless Stereo, not to be confused with True Wireless), which means you can connect multiple speakers together at the same time to create a stereo setup. </p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-sound-quality"><span>Tribit StormBox Micro 3 review: Sound quality</span></h2><p>For such a small speaker, the Tribit StormBox Micro 3 creates a big sound.  I think the speaker’s signature sound with the BassX setting already sounds great, but I got the best sound when using a custom EQ setting. </p><p>To test bass performance, I listened to <em>Tonight (I Wish I Was Your Boy)</em> by The 1975 using the BassX EQ. The punchy bass drives this song, and the speaker did a good job of keeping it controlled. It sounded clean while not overpowering the other components of the song, which still sounded sharp and balanced. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9WyziVp5FTGpjSMkxAbUvU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9WyziVp5FTGpjSMkxAbUvU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I then turned to the mid-performance of the speaker by listening to <em>Ok Love You Bye</em> by Olivia Dean. This track has a lot of layers that played perfectly with Tribit's signature EQ. The woodwind and brass instruments were very prominent throughout, and the vocals remained strong. I could hear how broad the soundstage is when listening to this song, and I was impressed by the performance. </p><p>Finally, I tested out the treble by listening to <em>In Bloom</em> by Neck Deep. This track is very bright with a complex percussion rhythm from the drums. For this track, I made a U-shaped  Custom EQ.  There were no issues with muddy sounds, and although there’s a lot going on, the instrument separation was excellent.</p><p>Overall, I was impressed with the way it played my favorite songs, but I wouldn’t recommend using the EQ presets, as some caused a lot of compression.</p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-app"><span>Tribit StormBox Micro 3 review: App</span></h2><p>The StormBox Micro 3 can be used with the Tribit app, which is available to download for free. </p><p>In the app, you’ll find six EQ presets you can choose from, or you can make your own custom EQ settings. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="HqSYnpzhSxC7pM3iP6eKtX" name="Tribit StormBox Micro 3 app" alt="Screenshots from the Tribit app showing the EQ controls of the Tribit StormBox Micro 3" src="https://cdn.mos.cms.futurecdn.net/HqSYnpzhSxC7pM3iP6eKtX.jpg" mos="" align="middle" fullscreen="" width="1510" height="850" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tribit)</span></figcaption></figure><p>The EQ uses nine bands, so there’s a lot of scope to make changes to the sound to fit your personal preference. </p><p>You can also set a sleep timer for automatic shut-off of the speaker, which is pretty useful, as well as see the remaining battery on the speaker. </p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-battery-life"><span>Tribit StormBox Micro 3 review: Battery life</span></h2><p>Speaking of battery life, the Tribit StormBox Micro 3 has a rated battery life of 24 hours when used at half volume. This is plenty loud enough for casual listening, but if you’re using the speaker outdoors and need to listen a little louder, the battery will then drain faster. </p><p>Still, 24 hours is plenty of battery, and four more hours than you get with the Tribit PocketGo. This battery life is far superior to the JBL Go 5, which only has 10 hours. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9JFp65GWDw98dPHu7u9yXU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/9JFp65GWDw98dPHu7u9yXU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Should you find yourself out of juice and in need of a quick boost, just 15 minutes of charging time with the included USB-C cable provides three hours of playback while a full charge takes around two hours. </p><p>Just like the Tribit StormBox Lava, the Micro 3 has reverse charging, which means you can use it as a portable charger in a pinch if you need to charge your phone.</p><h2 class="article-body__section" id="section-tribit-stormbox-micro-3-review-verdict"><span>Tribit StormBox Micro 3 review: Verdict</span></h2><p>Tribit has managed to make me love a budget speaker yet again. It's waterproof, dustproof, and drop-proof, making it an ideal option for anyone who wants to use the speaker outdoors. </p><p>Its companion app can be used for EQ customization, and the simple controls make it a breeze to use even without your phone nearby. The 24 hours of battery life mean you can listen all day, and the speaker can even be used as a portable charger in a pinch. But most importantly, the audio performance really is <em>excellent</em>.</p><p>For something slightly lighter, you could consider the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-clip-5-review">JBL Clip 5</a> for $79. This speaker also has a more sturdy clip at the top, rather than a flexible strap. The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> also gives beautiful sound for $54. The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ue-miniroll-review-tiny-but-mighty#section-ue-miniroll-review">UltimateEars Miniroll</a> costs $79 and also has a strap attachment at the back. This speaker is even lighter but is around the same size as the StormBox Micro 3. </p><p>For its price, however, the StormBox Micro 3 is tough to beat.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ You can take a portable Bluetooth speaker with you camping, but here's why it's not the best idea to use it on your hike ]]></title>
                                                                                                <dc:content><![CDATA[ <p>For me, hiking is a form of escapism. Getting out into nature is one of the easiest ways to switch off from the stressors of day-to-day life and reflect on what's happening, or block it out altogether. </p><p>But another way I like to unwind is by listening to music. It seems only natural to want to combine the two pastimes.</p><p>When I head to a campsite for the weekend, I usually like to bring one of the best Bluetooth speakers with me, like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a> or the <a href="https://tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit StormBox Micro 3</a>. And while it can be tempting to take the speaker into the woods to soundtrack a hike, that's probably not the best idea. Here's why. </p><h2 id="sound-travels-farther-than-you-think">Sound travels farther than you think</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZdvddFbSvYJEvrGg6Ske2V" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ZdvddFbSvYJEvrGg6Ske2V.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Even though it's just you and your speaker walking the trail, the sound it's making travels a lot farther than you'd think. In an open area, there's not much around to stop the sound from traveling, and not to mention the echo that can be caused by the shape of the landscape. </p><p>Many people, including myself, like to sit and listen to the sounds of their surroundings when on their walk. When the sound carries, it can disturb other people on their trail appreciating these sounds — or, worse, disturb the sleeping habits of nocturnal animals. </p><p>To combat this, it's best to use some of the <a href="https://www.tomsguide.com/best-picks/best-over-ear-headphones">best over-ear headphones</a> to keep the listening party private. </p><div class="product"><a data-dimension112="5b44dc22-2c41-4032-97df-df45fe93a3f3" data-action="Deal Block" data-label="Monitor III" data-dimension48="Monitor III" data-dimension25="$248" href="https://www.amazon.com/Marshall-Canceling-Over-Ear-Bluetooth-Headphones/dp/B0DFDT1TWC" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1080px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="xmTbqHyw3cvfuFwFbQYqQk" name="Marshall Monitor III prod image" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/xmTbqHyw3cvfuFwFbQYqQk.jpg" mos="" align="middle" fullscreen="" width="1080" height="1080" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Monitor III made our list of the <a href="https://www.tomsguide.com/us/best-headphones,review-1988.html" data-dimension112="5b44dc22-2c41-4032-97df-df45fe93a3f3" data-action="Deal Block" data-label="Monitor III" data-dimension48="Monitor III" data-dimension25="$248">best headphones</a> because of their incredible battery life. These retro-looking headphones deliver up to 100 hours of battery life are great for listening on the go. They're also extremely comfy, and we really appreciate their intuitive physical controls. <a class="view-deal button" href="https://www.amazon.com/Marshall-Canceling-Over-Ear-Bluetooth-Headphones/dp/B0DFDT1TWC" target="_blank" rel="nofollow" data-dimension112="5b44dc22-2c41-4032-97df-df45fe93a3f3" data-action="Deal Block" data-label="Monitor III" data-dimension48="Monitor III" data-dimension25="$248">View Deal</a></p></div><h2 id="better-safe-than-sorry">Better safe than sorry</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xvDCHhtwLtKyDJzMMtqPwZ" name="The Tribit PocketGo is the speaker that I will now be bringing with me everywhere, from picnics to hikes. It has awesome sound and can get super loud despite being compact and lightweight. It offers 20 hours of battery life, even with the incredibly loud volumes.  My only qualm with the speaker is that it requires some EQ alterations in the app, depending on the genre you’re listening to. But when it can offer a waterproof rating of IP68 and military-grade drop protection at such a low price, I can’t deny that this is the ultimate adventure speaker." alt="The Tribit PocketGo speaker in black photographed on a blue background." src="https://cdn.mos.cms.futurecdn.net/xvDCHhtwLtKyDJzMMtqPwZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This probably goes without saying, but when you're out walking, hiking, or running, you need to stay aware of your surroundings. Depending on the difficulty of the trail, it's important to stay aware of all of the sounds around you. </p><p>When you block out environmental sounds, you can miss out on important safety cues like wild animals, falling rocks, or even someone nearby who needs help. Even when listening at lower volumes, it can block out these ambient sounds. </p><p>I always think it's best to take in the sounds of nature too. As I mentioned, it helps me clear my mind and can be very good for lowering stress hormones. </p><p>A nice compromise here are open earbuds like the <a href="https://www.tomsguide.com/audio/earbuds/sony-linkbuds-clip-review">Sony LinkBuds Clip</a>, <a href="https://www.tomsguide.com/audio/earbuds/soundcore-aeroclip-review">Soundcore AeroClip</a> or <a href="https://www.tomsguide.com/audio/earbuds/bose-ultra-open-earbuds">Bose Ultra Open</a>. These earbuds let you listen to your music without disturbing nearby wildlife as well as let you keep an ear out for potential danger.</p><div class="product"><a data-dimension112="74f6b89c-edb7-487e-a7e1-8b503104177b" data-action="Deal Block" data-label="Ultra Open Earbuds" data-dimension48="Ultra Open Earbuds" data-dimension25="$229" href="https://www.amazon.com/dp/B0FDKR4RVP/?" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:679px;"><p class="vanilla-image-block" style="padding-top:62.59%;"><img id="wjSgGFFpyDJzdztK3MZ64S" name="Bose ultra open" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wjSgGFFpyDJzdztK3MZ64S.jpg" mos="" align="middle" fullscreen="" width="679" height="425" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/dp/B0FDKR4RVP/?" target="_blank" rel="nofollow" data-dimension112="74f6b89c-edb7-487e-a7e1-8b503104177b" data-action="Deal Block" data-label="Ultra Open Earbuds" data-dimension48="Ultra Open Earbuds" data-dimension25="$229">Ultra Open Earbuds: was $299 now $229</a></strong><br>The Bose Ultra Open perfected the open earbud formula. They have a comfortable, non-invasive fit, solid battery life, and they sound better than you might expect from open buds. They got <a href="https://www.tomsguide.com/audio/earbuds/bose-ultra-open-earbuds">4 stars in our review</a>, where we loved their simple controls and surprising lack of sound leakage. <a class="view-deal button" href="https://www.amazon.com/dp/B0FDKR4RVP/?" target="_blank" rel="nofollow" data-dimension112="74f6b89c-edb7-487e-a7e1-8b503104177b" data-action="Deal Block" data-label="Ultra Open Earbuds" data-dimension48="Ultra Open Earbuds" data-dimension25="$229">View Deal</a></p></div><h2 id="impact-on-wildlife">Impact on wildlife</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dLJQsthExUW6jmoyP5wHVU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/dLJQsthExUW6jmoyP5wHVU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Most importantly, if you care about wild animals' wellbeing, playing music out loud on trails can disrupt local wildlife. This can have a detrimental effect on the entire ecosystem and even lead to entire species being driven out of an area. </p><p>It impacts predators and prey in the wild alike, disrupting the food chain in the areas as well as breeding patterns. </p><div><blockquote><p>Activities such as finding desirable habitat and mates, avoiding predators, protecting young and establishing territories all depend on the acoustical environment.</p><p>The US National Park Service</p></blockquote></div><p>The <a href="https://www.nps.gov/subjects/sound/effects_wildlife.htm" target="_blank">United States government's National Park Service</a> states that sound plays an important role in ecosystems: "Activities such as finding desirable habitat and mates, avoiding predators, protecting young and establishing territories all depend on the acoustical environment."</p><p>Many speakers are marketed for use in the outdoors, and they do a good job at projecting sound when outside. If you're at the beach with friends or having a picnic they're great to have on-hand, but we should be mindful about where we use them. </p><p>Next time you're on the trail, keep the speaker back at camp and throw on a pair of headphones or open earbuds. Mother Nature will thank you for it.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OarDyX"></div>                            </div>                            <script src="https://kwizly.com/embed/OarDyX.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/entertainment/netflix/netflixs-new-rom-com-with-jennifer-lopez-and-ted-lassos-brett-goldstein-is-now-streaming-and-its-got-all-the-charm-of-a-corporate-training-video"><strong>Netflix’s new rom-com with Jennifer Lopez and ‘Ted Lasso’s’ Brett Goldstein is now streaming — and it’s got all the charm of a corporate training video</strong></a></li><li><a href="https://www.tomsguide.com/home/i-found-this-usd10-ikea-tool-in-the-smart-home-section-thatll-help-you-keep-cool-without-breaking-the-budget"><strong>I found this $10 IKEA tool in the smart home section that'll help you keep cool without breaking the budget</strong></a></li><li><a href="https://www.tomsguide.com/wellness/fitness-trackers/i-spent-two-weeks-with-the-usd99-fitbit-air-here-are-my-3-reasons-to-buy-and-3-reasons-to-skip"><strong>I spent two weeks with the $99 Fitbit Air — here are my 3 reasons to buy (and 3 reasons to skip)</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/you-can-take-a-portable-bluetooth-speaker-with-you-camping-but-heres-why-its-not-the-best-idea-to-use-it-on-your-hike</link>
                                                                            <description>
                            <![CDATA[ Many people love listening to music while on walks, but headphones are the best listening option so you don't disturb the wildlife. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">mbzDA9fGDG5Af56fMQAtKJ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/4Yq4rr3TLdoAT6dLRWsmJZ-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 08 Jun 2026 10:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ ashley.thieme@futurenet.com (Ashley Thieme) ]]></author>                    <dc:creator><![CDATA[ Ashley Thieme ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/3AWovHjApwuNrSGRS6WBcL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Ashley Thieme is a staff writer on the Reviews team at Tom’s Guide where she tests out the latest tech so you can know what’s going to be worth your time and money. She has a master’s degree in Magazine Journalism and a bachelor’s degree in Journalism, Media and Sociology from Cardiff University. She has bylines in titles including Women’s Health UK, sharing the latest health and wellness news stories, and Virgin Radio UK, providing the latest entertainment news and working on celebrity interviews. She has experience reporting on a variety of topics including music, literature, motorsport, entertainment and health. In previously published work, she has reviewed live music events, books, and wellness products. She values the importance of tech enhancing your life rather than taking over, and as a music fanatic, she is always looking for the best way to listen to new music releases. Discovering the top audio equipment that enhances sound quality and provides optimum comfort is what Ashley does best. In her spare time, Ashley enjoys hitting her reading goals on Goodreads by getting into the latest novels with a cup of tea as well as getting out in the Welsh mountains for a good hike on the weekend.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/4Yq4rr3TLdoAT6dLRWsmJZ-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide / Ashley Thieme]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Tribit StormBox Micro 3 Bluetooth Speaker and woman hiking on a trail]]></media:description>                                                            <media:text><![CDATA[Tribit StormBox Micro 3 Bluetooth Speaker and woman hiking on a trail]]></media:text>
                                <media:title type="plain"><![CDATA[Tribit StormBox Micro 3 Bluetooth Speaker and woman hiking on a trail]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/4Yq4rr3TLdoAT6dLRWsmJZ-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>For me, hiking is a form of escapism. Getting out into nature is one of the easiest ways to switch off from the stressors of day-to-day life and reflect on what's happening, or block it out altogether. </p><p>But another way I like to unwind is by listening to music. It seems only natural to want to combine the two pastimes.</p><p>When I head to a campsite for the weekend, I usually like to bring one of the best Bluetooth speakers with me, like the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a> or the <a href="https://tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-micro-3-review">Tribit StormBox Micro 3</a>. And while it can be tempting to take the speaker into the woods to soundtrack a hike, that's probably not the best idea. Here's why. </p><h2 id="sound-travels-farther-than-you-think">Sound travels farther than you think</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZdvddFbSvYJEvrGg6Ske2V" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ZdvddFbSvYJEvrGg6Ske2V.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Even though it's just you and your speaker walking the trail, the sound it's making travels a lot farther than you'd think. In an open area, there's not much around to stop the sound from traveling, and not to mention the echo that can be caused by the shape of the landscape. </p><p>Many people, including myself, like to sit and listen to the sounds of their surroundings when on their walk. When the sound carries, it can disturb other people on their trail appreciating these sounds — or, worse, disturb the sleeping habits of nocturnal animals. </p><p>To combat this, it's best to use some of the <a href="https://www.tomsguide.com/best-picks/best-over-ear-headphones">best over-ear headphones</a> to keep the listening party private. </p><div class="product"><a data-dimension112="5b44dc22-2c41-4032-97df-df45fe93a3f3" data-action="Deal Block" data-label="Monitor III" data-dimension48="Monitor III" data-dimension25="$248" href="https://www.amazon.com/Marshall-Canceling-Over-Ear-Bluetooth-Headphones/dp/B0DFDT1TWC" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1080px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="xmTbqHyw3cvfuFwFbQYqQk" name="Marshall Monitor III prod image" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/xmTbqHyw3cvfuFwFbQYqQk.jpg" mos="" align="middle" fullscreen="" width="1080" height="1080" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Monitor III made our list of the <a href="https://www.tomsguide.com/us/best-headphones,review-1988.html" data-dimension112="5b44dc22-2c41-4032-97df-df45fe93a3f3" data-action="Deal Block" data-label="Monitor III" data-dimension48="Monitor III" data-dimension25="$248">best headphones</a> because of their incredible battery life. These retro-looking headphones deliver up to 100 hours of battery life are great for listening on the go. They're also extremely comfy, and we really appreciate their intuitive physical controls. <a class="view-deal button" href="https://www.amazon.com/Marshall-Canceling-Over-Ear-Bluetooth-Headphones/dp/B0DFDT1TWC" target="_blank" rel="nofollow" data-dimension112="5b44dc22-2c41-4032-97df-df45fe93a3f3" data-action="Deal Block" data-label="Monitor III" data-dimension48="Monitor III" data-dimension25="$248">View Deal</a></p></div><h2 id="better-safe-than-sorry">Better safe than sorry</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xvDCHhtwLtKyDJzMMtqPwZ" name="The Tribit PocketGo is the speaker that I will now be bringing with me everywhere, from picnics to hikes. It has awesome sound and can get super loud despite being compact and lightweight. It offers 20 hours of battery life, even with the incredibly loud volumes.  My only qualm with the speaker is that it requires some EQ alterations in the app, depending on the genre you’re listening to. But when it can offer a waterproof rating of IP68 and military-grade drop protection at such a low price, I can’t deny that this is the ultimate adventure speaker." alt="The Tribit PocketGo speaker in black photographed on a blue background." src="https://cdn.mos.cms.futurecdn.net/xvDCHhtwLtKyDJzMMtqPwZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This probably goes without saying, but when you're out walking, hiking, or running, you need to stay aware of your surroundings. Depending on the difficulty of the trail, it's important to stay aware of all of the sounds around you. </p><p>When you block out environmental sounds, you can miss out on important safety cues like wild animals, falling rocks, or even someone nearby who needs help. Even when listening at lower volumes, it can block out these ambient sounds. </p><p>I always think it's best to take in the sounds of nature too. As I mentioned, it helps me clear my mind and can be very good for lowering stress hormones. </p><p>A nice compromise here are open earbuds like the <a href="https://www.tomsguide.com/audio/earbuds/sony-linkbuds-clip-review">Sony LinkBuds Clip</a>, <a href="https://www.tomsguide.com/audio/earbuds/soundcore-aeroclip-review">Soundcore AeroClip</a> or <a href="https://www.tomsguide.com/audio/earbuds/bose-ultra-open-earbuds">Bose Ultra Open</a>. These earbuds let you listen to your music without disturbing nearby wildlife as well as let you keep an ear out for potential danger.</p><div class="product"><a data-dimension112="74f6b89c-edb7-487e-a7e1-8b503104177b" data-action="Deal Block" data-label="Ultra Open Earbuds" data-dimension48="Ultra Open Earbuds" data-dimension25="$229" href="https://www.amazon.com/dp/B0FDKR4RVP/?" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:679px;"><p class="vanilla-image-block" style="padding-top:62.59%;"><img id="wjSgGFFpyDJzdztK3MZ64S" name="Bose ultra open" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wjSgGFFpyDJzdztK3MZ64S.jpg" mos="" align="middle" fullscreen="" width="679" height="425" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/dp/B0FDKR4RVP/?" target="_blank" rel="nofollow" data-dimension112="74f6b89c-edb7-487e-a7e1-8b503104177b" data-action="Deal Block" data-label="Ultra Open Earbuds" data-dimension48="Ultra Open Earbuds" data-dimension25="$229">Ultra Open Earbuds: was $299 now $229</a></strong><br>The Bose Ultra Open perfected the open earbud formula. They have a comfortable, non-invasive fit, solid battery life, and they sound better than you might expect from open buds. They got <a href="https://www.tomsguide.com/audio/earbuds/bose-ultra-open-earbuds">4 stars in our review</a>, where we loved their simple controls and surprising lack of sound leakage. <a class="view-deal button" href="https://www.amazon.com/dp/B0FDKR4RVP/?" target="_blank" rel="nofollow" data-dimension112="74f6b89c-edb7-487e-a7e1-8b503104177b" data-action="Deal Block" data-label="Ultra Open Earbuds" data-dimension48="Ultra Open Earbuds" data-dimension25="$229">View Deal</a></p></div><h2 id="impact-on-wildlife">Impact on wildlife</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dLJQsthExUW6jmoyP5wHVU" name="Tribit StormBox Micro 3" alt="Tribit StormBox Micro 3 in black photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/dLJQsthExUW6jmoyP5wHVU.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Most importantly, if you care about wild animals' wellbeing, playing music out loud on trails can disrupt local wildlife. This can have a detrimental effect on the entire ecosystem and even lead to entire species being driven out of an area. </p><p>It impacts predators and prey in the wild alike, disrupting the food chain in the areas as well as breeding patterns. </p><div><blockquote><p>Activities such as finding desirable habitat and mates, avoiding predators, protecting young and establishing territories all depend on the acoustical environment.</p><p>The US National Park Service</p></blockquote></div><p>The <a href="https://www.nps.gov/subjects/sound/effects_wildlife.htm" target="_blank">United States government's National Park Service</a> states that sound plays an important role in ecosystems: "Activities such as finding desirable habitat and mates, avoiding predators, protecting young and establishing territories all depend on the acoustical environment."</p><p>Many speakers are marketed for use in the outdoors, and they do a good job at projecting sound when outside. If you're at the beach with friends or having a picnic they're great to have on-hand, but we should be mindful about where we use them. </p><p>Next time you're on the trail, keep the speaker back at camp and throw on a pair of headphones or open earbuds. Mother Nature will thank you for it.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OarDyX"></div>                            </div>                            <script src="https://kwizly.com/embed/OarDyX.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/entertainment/netflix/netflixs-new-rom-com-with-jennifer-lopez-and-ted-lassos-brett-goldstein-is-now-streaming-and-its-got-all-the-charm-of-a-corporate-training-video"><strong>Netflix’s new rom-com with Jennifer Lopez and ‘Ted Lasso’s’ Brett Goldstein is now streaming — and it’s got all the charm of a corporate training video</strong></a></li><li><a href="https://www.tomsguide.com/home/i-found-this-usd10-ikea-tool-in-the-smart-home-section-thatll-help-you-keep-cool-without-breaking-the-budget"><strong>I found this $10 IKEA tool in the smart home section that'll help you keep cool without breaking the budget</strong></a></li><li><a href="https://www.tomsguide.com/wellness/fitness-trackers/i-spent-two-weeks-with-the-usd99-fitbit-air-here-are-my-3-reasons-to-buy-and-3-reasons-to-skip"><strong>I spent two weeks with the $99 Fitbit Air — here are my 3 reasons to buy (and 3 reasons to skip)</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Don't suffer through muffled commentary this World Cup — these 5 soundbars will ensure you hear every goal (from just $53) ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The World Cup is the biggest worldwide sporting event of the year, and if you're a soccer fan, you're going to want to make sure you don't miss a thing. We've already got our <a href="https://www.tomsguide.com/tvs/as-a-tv-expert-i-found-my-top-10-deals-for-world-cup-2026-watch-parties-and-they-start-at-just-usd349">favorite TV deals</a> down pat, so let's turn our focus to the equally important soundbars. </p><p>If you want great sound quality for less, I've rounded up my favorite 5 <a href="https://www.tomsguide.com/best-picks/best-cheap-soundbars">cheap soundbars</a>, including the $53 <a href="https://www.tomsguide.com/audio/soundbars/majority-naga-60-review">Majority Naga 60</a> and the top-tier $99 <a href="https://www.tomsguide.com/audio/soundbars/sony-ht-s100f-review">Sony HT-S100F</a>. Although these soundbars don't have "premium" features like Dolby Atmos or AirPlay/Google Cast, they're a fantastic upgrade from tinny, thin TV speakers — and more than enough for the World Cup.</p><p>Because the World Cup is such a rare event, it's the perfect occasion to go all out. I remember the run-up to the 2010 World Cup like it was yesterday. Until the day before kick-off, we had a tiny, very '90s TV with four channels. Then my dad returned from Curry's (the U.K.'s Best Buy) with a lovely flatscreen and announced we would be watching the World Cup in style. Of course, England didn't win, but at least we saw the players' tears in HD. </p>        <div class="featured_product_block featured_block_hero" data-id="163d8e8f-8412-4ace-8c36-c1f7d6d32b4d">            <a href="https://www.amazon.com/Majority-Bluetooth-Soundbar-Subwoofer-Mountable/dp/B0CR7XS8MV" data-model-name="Naga 60 Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:120.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/jLxV3cXoGyrxaxLNQrYgzi.jpg" alt="Majority Naga 60"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Majority</div>                                        <div class="featured__title">Naga 60 Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="0a224542-cd3c-4025-bec5-528cdabcc696">            <a href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" data-model-name="HT-S100F soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:66.67%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/yGJi3on3RmkZgfbENVxnTV.jpg" alt="Sony Ht-S100f 120w Sound Bar - Black - as New - Refurbished"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">HT-S100F soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c321b291-3b0c-45ac-ac4c-f437826d442c">            <a href="https://www.amazon.com/JBL-Bar-2-0-All-one/dp/B0BQPPQKQ5" data-model-name="Bar 2.0" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/Yc5ZQFcPZUxh4gyhgP3LsX.jpg" alt="the JBL bar 2.0 soundbar with a remote on a white square background"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>JBL</div>                                        <div class="featured__title">Bar 2.0</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2647e4b8-52f9-4ac3-8848-699379cf0767">            <a href="https://www.bestbuy.com/product/samsung-b-series-hw-b650f-3-1-ch-dts-virtualx-soundbar-black/J3ZYG2FV28/sku/6619391" data-model-name="B650" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/RFuy9EY8WNkD8jriP4sfnS.jpg" alt="Samsung HW-B650 Soundbar"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Samsung</div>                                        <div class="featured__title">B650</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="acae1e2c-dc21-4457-8d98-f6a8bc53207b">            <a href="https://www.amazon.com/Bose-Speaker-Soundbar-Bluetooth-connectivity/dp/B088KRPCQJ" data-model-name="TV Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:39.47%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/AHTTmLHs5odVXSuSHG9SB9.png" alt="Bose, TV speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Bose</div>                                        <div class="featured__title">TV Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div><h2 id="which-soundbar-is-best-for-me">Which soundbar is best for me? </h2><p>If you're on a super low budget, I'd recommend the Majority Naga 60. I reviewed this unbelievably cheap soundbar last year and was genuinely blown away by the sound. Of course, it doesn't sound as rich or warm as, say, the Bose Smart Soundbar ($549) — but it's literally $500 cheaper. </p><p>For $50, it doesn't get much better than this. </p><p>If you've got a little more to spend, you should definitely look at the Sony HTS-100F. I love this fierce little soundbar. It's loud, with fantastic dialogue clarity, and would be my personal preference from this list. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DUinHpC2RKAgJKy6std6CZ" name="Sony_HTSF150_soundbar_ 2.JPG" alt="the sony ht-s100f or sony ht-sf150 in uk, a black soundbar with buttons on the top, a remote, photographed with a panasonic TV" src="https://cdn.mos.cms.futurecdn.net/DUinHpC2RKAgJKy6std6CZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Then, if you've got a little more moolah in your pocket, I'd suggest going for the JBL Bar 2.0. On sale, it's just $149, but it has an MSRP of $219. This soundbar has immersive, room-filling sound and Dolby Digital — kind of like "fake" Dolby Atmos. </p><p>At the higher end, the <a href="https://www.tomsguide.com/audio/soundbars/samsung-hw-b650-soundbar-review">Samsung B650</a> also will not disappoint you. This $250 soundbar has surprisingly excellent surround sound with DTS:Virtual X and super-clear dialogue. </p><p>For those who really value a big brand, the Bose TV speaker is the best choice. This $289 soundbar combines classic Bose sound quality with an 100W speaker and a dedicated vocal enhancer — perfect for watching sports. </p><p>Which soundbar would you go for? Personally, I'd get the Sony HTS-100F — it's one of the best bang-for-buck soundbars on the market right now. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/dont-suffer-through-muffled-commentary-this-world-cup-these-5-soundbars-will-ensure-you-hear-every-goal-from-just-usd53</link>
                                                                            <description>
                            <![CDATA[ I test tons of soundbars every year and these are my top 5 recommendations for crystal-clear sound all World Cup long. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">CN3CY8pX46dvAsofZpFcAJ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/Rrbszb4zj7Pwufm3depNbg-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sun, 07 Jun 2026 11:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ erin.bashford@futurenet.com (Erin Bashford) ]]></author>                    <dc:creator><![CDATA[ Erin Bashford ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/rLvJvJVZx43hEzSsJy3BpL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Erin Bashford is a senior reviews writer at Tom’s Guide. She has a Master’s in Broadcast and Digital Journalism from the University of East Anglia and 7 years of experience reviewing music and events for various publications. She has edited publications such as Outline Magazine’s Guide to Norwich, and she has written for a number of music magazines and websites such as Clash Magazine, Outline Magazine and Dork Magazine. She has a strong interest in audio gear and the music world. &lt;/p&gt;&lt;p&gt;As an ex-barista, Erin is passionate about coffee tech. She also loves finding the best cooking hacks and kitchen appliances, including her beloved Instant Pot. &lt;/p&gt;&lt;p&gt;In her spare time, you can find her reading, practising yoga, hiking, writing fantasy novels, or stressing over NYT Games.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/Rrbszb4zj7Pwufm3depNbg-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[the sony ht-s100f with a savings squad badge]]></media:description>                                                            <media:text><![CDATA[the sony ht-s100f with a savings squad badge]]></media:text>
                                <media:title type="plain"><![CDATA[the sony ht-s100f with a savings squad badge]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/Rrbszb4zj7Pwufm3depNbg-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The World Cup is the biggest worldwide sporting event of the year, and if you're a soccer fan, you're going to want to make sure you don't miss a thing. We've already got our <a href="https://www.tomsguide.com/tvs/as-a-tv-expert-i-found-my-top-10-deals-for-world-cup-2026-watch-parties-and-they-start-at-just-usd349">favorite TV deals</a> down pat, so let's turn our focus to the equally important soundbars. </p><p>If you want great sound quality for less, I've rounded up my favorite 5 <a href="https://www.tomsguide.com/best-picks/best-cheap-soundbars">cheap soundbars</a>, including the $53 <a href="https://www.tomsguide.com/audio/soundbars/majority-naga-60-review">Majority Naga 60</a> and the top-tier $99 <a href="https://www.tomsguide.com/audio/soundbars/sony-ht-s100f-review">Sony HT-S100F</a>. Although these soundbars don't have "premium" features like Dolby Atmos or AirPlay/Google Cast, they're a fantastic upgrade from tinny, thin TV speakers — and more than enough for the World Cup.</p><p>Because the World Cup is such a rare event, it's the perfect occasion to go all out. I remember the run-up to the 2010 World Cup like it was yesterday. Until the day before kick-off, we had a tiny, very '90s TV with four channels. Then my dad returned from Curry's (the U.K.'s Best Buy) with a lovely flatscreen and announced we would be watching the World Cup in style. Of course, England didn't win, but at least we saw the players' tears in HD. </p>        <div class="featured_product_block featured_block_hero" data-id="163d8e8f-8412-4ace-8c36-c1f7d6d32b4d">            <a href="https://www.amazon.com/Majority-Bluetooth-Soundbar-Subwoofer-Mountable/dp/B0CR7XS8MV" data-model-name="Naga 60 Soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:120.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/jLxV3cXoGyrxaxLNQrYgzi.jpg" alt="Majority Naga 60"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Majority</div>                                        <div class="featured__title">Naga 60 Soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="0a224542-cd3c-4025-bec5-528cdabcc696">            <a href="https://www.amazon.com/Sony-Speaker-Integrated-Bluetooth-HTS100F/dp/B0797MY6JB" data-model-name="HT-S100F soundbar" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:66.67%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/yGJi3on3RmkZgfbENVxnTV.jpg" alt="Sony Ht-S100f 120w Sound Bar - Black - as New - Refurbished"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Sony</div>                                        <div class="featured__title">HT-S100F soundbar</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="c321b291-3b0c-45ac-ac4c-f437826d442c">            <a href="https://www.amazon.com/JBL-Bar-2-0-All-one/dp/B0BQPPQKQ5" data-model-name="Bar 2.0" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/Yc5ZQFcPZUxh4gyhgP3LsX.jpg" alt="the JBL bar 2.0 soundbar with a remote on a white square background"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>JBL</div>                                        <div class="featured__title">Bar 2.0</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2647e4b8-52f9-4ac3-8848-699379cf0767">            <a href="https://www.bestbuy.com/product/samsung-b-series-hw-b650f-3-1-ch-dts-virtualx-soundbar-black/J3ZYG2FV28/sku/6619391" data-model-name="B650" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:100.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/RFuy9EY8WNkD8jriP4sfnS.jpg" alt="Samsung HW-B650 Soundbar"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Samsung</div>                                        <div class="featured__title">B650</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="acae1e2c-dc21-4457-8d98-f6a8bc53207b">            <a href="https://www.amazon.com/Bose-Speaker-Soundbar-Bluetooth-connectivity/dp/B088KRPCQJ" data-model-name="TV Speaker" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:39.47%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/AHTTmLHs5odVXSuSHG9SB9.png" alt="Bose, TV speaker"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>Bose</div>                                        <div class="featured__title">TV Speaker</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div><h2 id="which-soundbar-is-best-for-me">Which soundbar is best for me? </h2><p>If you're on a super low budget, I'd recommend the Majority Naga 60. I reviewed this unbelievably cheap soundbar last year and was genuinely blown away by the sound. Of course, it doesn't sound as rich or warm as, say, the Bose Smart Soundbar ($549) — but it's literally $500 cheaper. </p><p>For $50, it doesn't get much better than this. </p><p>If you've got a little more to spend, you should definitely look at the Sony HTS-100F. I love this fierce little soundbar. It's loud, with fantastic dialogue clarity, and would be my personal preference from this list. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DUinHpC2RKAgJKy6std6CZ" name="Sony_HTSF150_soundbar_ 2.JPG" alt="the sony ht-s100f or sony ht-sf150 in uk, a black soundbar with buttons on the top, a remote, photographed with a panasonic TV" src="https://cdn.mos.cms.futurecdn.net/DUinHpC2RKAgJKy6std6CZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Then, if you've got a little more moolah in your pocket, I'd suggest going for the JBL Bar 2.0. On sale, it's just $149, but it has an MSRP of $219. This soundbar has immersive, room-filling sound and Dolby Digital — kind of like "fake" Dolby Atmos. </p><p>At the higher end, the <a href="https://www.tomsguide.com/audio/soundbars/samsung-hw-b650-soundbar-review">Samsung B650</a> also will not disappoint you. This $250 soundbar has surprisingly excellent surround sound with DTS:Virtual X and super-clear dialogue. </p><p>For those who really value a big brand, the Bose TV speaker is the best choice. This $289 soundbar combines classic Bose sound quality with an 100W speaker and a dedicated vocal enhancer — perfect for watching sports. </p><p>Which soundbar would you go for? Personally, I'd get the Sony HTS-100F — it's one of the best bang-for-buck soundbars on the market right now. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ 5 best new products from High-End — the world's biggest audio show ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The High End audio show is on! I've been at High End Vienna for two days now, and I've seen my fair share of wildly expensive audio gear (it's called High End for a reason), including a $3.8 million speaker system.</p><p>Personally, I prefer the more attainable audio gear. With that in mind, I've rounded up a few of my favorite products from the show, from <a href="https://www.tomsguide.com/us/best-noise-cancelling-headphones,review-5566.html">headphones</a> to <a href="https://www.tomsguide.com/best-picks/best-record-players">turntables</a>. These were either launched at the show, or are fairly new and being shown off properly for the first time. Looking for the latest and greatest in audio? I've got you covered.</p><h2 id="1-noble-fokus-artemis">1. Noble Fokus Artemis</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/GHF6rcQSDSJxjv8kVR4uxG.jpg" alt="Noble Fokus Artemis" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Yt6yRyN5hA6VbMSTqqiqJH.jpg" alt="Noble Fokus Artemis" /><figcaption><small role="credit">Future</small></figcaption></figure></figure><p>These are without doubt my favorite headphones from this year's show. I spent some time talking to Kai, Noble Audio's Director of Marketing, who ran me through these new cans.</p><p>Building on the success of Noble's Apollo over-ears, the Fokus Artemis are designed to appeal to the general user just as much as the public. "We really wanted something that was for the people," Kai said. </p><p>The drivers have been tweaked from the Apollo. In fact, there are three in each earcup: a planar magnetic driver, a dynamic driver and a balanced armature driver, to give a much wider spread of sound.</p><p>They're super comfortable, IP52-rated, feature a replaceable battery and can be tweaked with a 5-band parametric EQ via a companion app. The EQ data is also stored on the headphones, so you can theoretically delete the app and your custom EQ will still run. </p><p>The best part is that they can utilize an automatic EQ that can compensate for hearing loss. So, if you have a hearing imbalance, the EQ will automatically adjust per side to ensure the sound is balanced. Very cool.</p><p>The Fokus Artemis will cost <a href="https://nobleaudio.com/" target="_blank" rel="nofollow">$899 / £799 at Noble Audio</a> and will start shipping in July.</p><h2 id="2-meze-arta">2. Meze Arta</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9984px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5CBqGZwXAqszag2VhaNhdZ" name="DSC08394" alt="Meze Arta" src="https://cdn.mos.cms.futurecdn.net/5CBqGZwXAqszag2VhaNhdZ.jpg" mos="" align="middle" fullscreen="" width="9984" height="5616" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Another set of cans, albeit a lot more pricey: Meze Audio has launched the ARTA, which I got to try out at High End. These things are utterly exquisite — something that applies to all Meze gear, even my "budget" <a href="https://www.tomsguide.com/audio/i-just-tested-these-wired-headphones-and-they-sound-absolutely-stunning-without-breaking-the-bank">Meze 105 AER.</a></p><p>The ARTA are open-back, planar magnetic beasts that just exude luxury with their gorgeous art nouveau design. The sound profile is exceptionally clean, with as much detail as you could ask for. They're warm, though, and aren't lacking in bass when you want it.</p><p>There are a couple of issues, however. Firstly, there's the price. Top-tier Meze gear never comes cheap, and the ARTA are no exception. You'll need a cool $6,000 to get your hands on them. These cans have a very high resistance of 225-ohms, too, making them extremely tricky to drive — so add on the price of upgrading your DAC/amp.</p><p>Stay tuned to <a href="https://mezeaudio.com/pages/arta?srsltid=AfmBOoriIOxPmweE1nXtTe8yW5A-IQ4Q0i3r_ltc25RAgFj-x54bA6dX" target="_blank" rel="nofollow">Meze</a>'s official channels for estimated shipping timelines.</p><h2 id="3-kanto-obi3">3. Kanto OBI3</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9984px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JaCci38CaLZZbuMhxcSrTZ" name="DSC08377" alt="Kanto OBI3 on a white table" src="https://cdn.mos.cms.futurecdn.net/JaCci38CaLZZbuMhxcSrTZ.jpg" mos="" align="middle" fullscreen="" width="9984" height="5616" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Alright, this one was known about before High End, but I got to see Kanto's new budget turntable, the <a href="https://kantoaudio.com/turntables/obi3/" target="_blank" rel="nofollow">OBI3</a>, and even talk to the guy who designed it!</p><p>The cheap, entry-level turntable market has plenty of options, but when you dig a little deeper, you'll find that most are all the same, kinda crappy units, reskinned, rebadged and repackaged. </p><p>Kanto set out to make its own turntable for the budget market that was built by them, not someone else. It uses an Audio Technica 3600L cartridge and costs $199, meaning newcomers to vinyl can grab a turntable and a budget speaker, like <a href="https://www.amazon.com/Kanto-YU-Bookshelf-Bluetooth-Detection/dp/B084YZKVTB/" target="_blank" rel="nofollow">Kanto's $349 YU</a>, for around $550. Not bad. </p><h2 id="4-ruark-r710">4. Ruark R710</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7bFLBVGBoiWdKfvjwAkLp5" name="DSC08347" alt="Ruark R710" src="https://cdn.mos.cms.futurecdn.net/7bFLBVGBoiWdKfvjwAkLp5.jpg" mos="" align="middle" fullscreen="" width="9600" height="5400" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>"The world is crying out for CD players," said Ruark's Design Manager, Jan Paoli, during the launch of the Ruark R710 media console at High End. </p><p>British high-end (but attainable) hi-fi manufacturer Ruark has been making radios and CD players for a long time, which is rather fortuitous given the (re)rising popularity of CDs right now.</p><p>The R710 is a music console, delivering 200W of power per channel and offering a wealth of connectivity options, including wireless stmreaing via Spotify Connect, Tidal Connect and Qobuz connect. </p><p>It offers hi-res streaming up to 32-bit/192kHz, features a phono stage for hooking up turntables, and is designed to be used with the new Ruark Talisman-R floor speakers — which I was treated to a demo of, and which sound utterly lovely, with a warm-but-detailed hi-fi sound.</p><p>The R710 will go on sale in September at £2,200, while the Talisman-R speakers will follow shortly afterwards in October, priced at £1,500. They'll be available in the U.S., although pricing has yet to be confirmed.</p><h2 id="5-ifi-gr2">5. iFi GR2</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9984px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NkigHWNHdcyr86fia2abBa" name="DSC08410" alt="iFi GR2" src="https://cdn.mos.cms.futurecdn.net/NkigHWNHdcyr86fia2abBa.jpg" mos="" align="middle" fullscreen="" width="9984" height="5616" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>"A Hip-Dac 3 on steroids." That's how iFi's engineers described the new iFi GR2, and it's spot on. I own an iFi Hip-Dac 3, and love it, but the GR2 is a whole different beast. </p><p>Delivering 1,513mW of power, or 2,173mW in its max power mode, the GR2 is intended to be a pocketable powerhouse for hard-to-drive headphones.</p><p>Admittedly, it probably won't power the Meze ARTA above, but otherwise there aren't many headphones it can't drive, according to the engineers.</p><p>The GR2 takes what I love about the Hip-Dac 3 — its portability and simplicity of use — beefs it up with a touch screen, and adds more power. </p><p>In addition to iFi's Xbass mode, the GR2 also has an Xspace mode, which increases the width and spaciousness of sound. It worked extremely well in the ultra-spatial "Antigravity" by Sohn.</p><p>The <a href="https://ifi-audio.com/products/idsd-gr-2?srsltid=AfmBOorXfZLbtxR5Il2mV2f9IrQGvOru7txkrRY6czz3Rx2AUFy82goa" target="_blank" rel="nofollow">GR2 will cost $529</a>, can be pre-ordered now, and is expected to start shipping in July 2026.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-W21yoe"></div>                            </div>                            <script src="https://kwizly.com/embed/W21yoe.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/earbuds/i-never-thought-cheap-wired-earbuds-could-sound-so-good-heres-how-the-usd39-dunu-titan-x-change-the-budget-hi-res-game">‘I never thought cheap wired earbuds could sound so good’: here's how the $39 Dunu Titan X change the budget hi-res game</a></li><li><a href="https://www.tomsguide.com/audio/headphones/shokz-opendots-2-review">I tested the new Shokz OpenDots 2 headphones for three weeks and they’re better value than the Bose Open Ultra Earbuds</a></li><li><a href="https://www.tomsguide.com/audio/headphones/jbl-live-780nc-review">I tested JBL’s new mid-range headphones — and they’re zingy, fresh, and fun (and finally have hi-res audio!)</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/5-best-new-products-from-the-worlds-biggest-high-end-audio-show</link>
                                                                            <description>
                            <![CDATA[ Here are my favorite new products from the first High End audio show to be held in Vienna. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">AAZ588ZXBiD8dKL2jL7p58</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/HpK7J4x5RpisbFwaZtSQbZ-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 06 Jun 2026 11:30:00 +0000</pubDate>                                                                                                                                <updated>Mon, 08 Jun 2026 08:11:14 +0000</updated>
                                                                                                                                            <category><![CDATA[Audio]]></category>
                                                                                                <author><![CDATA[ peter.wolinski@futurenet.com (Peter Wolinski) ]]></author>                    <dc:creator><![CDATA[ Peter Wolinski ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/stgPfXWY7ukw8J8rfC7vjg.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Peter is a Senior Editor at Tom&#039;s Guide, heading up the site&#039;s Reviews team and Cameras section. Having built gaming PCs since he was 10 (that&#039;s a while ago now) he&#039;s a bit of a nerd about components and hardware. He&#039;s also been an iPhone user since the classic iPhone 4, and a Mac user for well over a decade. Experienced in using and testing all kinds of technology — from phones through to tablets, computers, games consoles, cameras and smart home tech — helping people find the best tech for them (at the best prices) is what Peter does best. A photographer since he bought his first camera (a Fujifilm) in 2015, Peter was previously an Editor for Canon-Europe.com. He then edited the Cameras and How To sections of Tom&#039;s Guide. When he&#039;s not crafting helpful, in-depth reviews, Peter can usually be found out and about honing his architectural photography skills, riding his motorcycle around Welsh mountain roads, telling everyone about his two greyhounds, squeezing a few extra FPS out of PC games or perfecting his espresso shots.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/HpK7J4x5RpisbFwaZtSQbZ-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Noble Fokus Artemis]]></media:description>                                                            <media:text><![CDATA[Noble Fokus Artemis]]></media:text>
                                <media:title type="plain"><![CDATA[Noble Fokus Artemis]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/HpK7J4x5RpisbFwaZtSQbZ-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The High End audio show is on! I've been at High End Vienna for two days now, and I've seen my fair share of wildly expensive audio gear (it's called High End for a reason), including a $3.8 million speaker system.</p><p>Personally, I prefer the more attainable audio gear. With that in mind, I've rounded up a few of my favorite products from the show, from <a href="https://www.tomsguide.com/us/best-noise-cancelling-headphones,review-5566.html">headphones</a> to <a href="https://www.tomsguide.com/best-picks/best-record-players">turntables</a>. These were either launched at the show, or are fairly new and being shown off properly for the first time. Looking for the latest and greatest in audio? I've got you covered.</p><h2 id="1-noble-fokus-artemis">1. Noble Fokus Artemis</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/GHF6rcQSDSJxjv8kVR4uxG.jpg" alt="Noble Fokus Artemis" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Yt6yRyN5hA6VbMSTqqiqJH.jpg" alt="Noble Fokus Artemis" /><figcaption><small role="credit">Future</small></figcaption></figure></figure><p>These are without doubt my favorite headphones from this year's show. I spent some time talking to Kai, Noble Audio's Director of Marketing, who ran me through these new cans.</p><p>Building on the success of Noble's Apollo over-ears, the Fokus Artemis are designed to appeal to the general user just as much as the public. "We really wanted something that was for the people," Kai said. </p><p>The drivers have been tweaked from the Apollo. In fact, there are three in each earcup: a planar magnetic driver, a dynamic driver and a balanced armature driver, to give a much wider spread of sound.</p><p>They're super comfortable, IP52-rated, feature a replaceable battery and can be tweaked with a 5-band parametric EQ via a companion app. The EQ data is also stored on the headphones, so you can theoretically delete the app and your custom EQ will still run. </p><p>The best part is that they can utilize an automatic EQ that can compensate for hearing loss. So, if you have a hearing imbalance, the EQ will automatically adjust per side to ensure the sound is balanced. Very cool.</p><p>The Fokus Artemis will cost <a href="https://nobleaudio.com/" target="_blank" rel="nofollow">$899 / £799 at Noble Audio</a> and will start shipping in July.</p><h2 id="2-meze-arta">2. Meze Arta</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9984px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5CBqGZwXAqszag2VhaNhdZ" name="DSC08394" alt="Meze Arta" src="https://cdn.mos.cms.futurecdn.net/5CBqGZwXAqszag2VhaNhdZ.jpg" mos="" align="middle" fullscreen="" width="9984" height="5616" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Another set of cans, albeit a lot more pricey: Meze Audio has launched the ARTA, which I got to try out at High End. These things are utterly exquisite — something that applies to all Meze gear, even my "budget" <a href="https://www.tomsguide.com/audio/i-just-tested-these-wired-headphones-and-they-sound-absolutely-stunning-without-breaking-the-bank">Meze 105 AER.</a></p><p>The ARTA are open-back, planar magnetic beasts that just exude luxury with their gorgeous art nouveau design. The sound profile is exceptionally clean, with as much detail as you could ask for. They're warm, though, and aren't lacking in bass when you want it.</p><p>There are a couple of issues, however. Firstly, there's the price. Top-tier Meze gear never comes cheap, and the ARTA are no exception. You'll need a cool $6,000 to get your hands on them. These cans have a very high resistance of 225-ohms, too, making them extremely tricky to drive — so add on the price of upgrading your DAC/amp.</p><p>Stay tuned to <a href="https://mezeaudio.com/pages/arta?srsltid=AfmBOoriIOxPmweE1nXtTe8yW5A-IQ4Q0i3r_ltc25RAgFj-x54bA6dX" target="_blank" rel="nofollow">Meze</a>'s official channels for estimated shipping timelines.</p><h2 id="3-kanto-obi3">3. Kanto OBI3</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9984px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="JaCci38CaLZZbuMhxcSrTZ" name="DSC08377" alt="Kanto OBI3 on a white table" src="https://cdn.mos.cms.futurecdn.net/JaCci38CaLZZbuMhxcSrTZ.jpg" mos="" align="middle" fullscreen="" width="9984" height="5616" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Alright, this one was known about before High End, but I got to see Kanto's new budget turntable, the <a href="https://kantoaudio.com/turntables/obi3/" target="_blank" rel="nofollow">OBI3</a>, and even talk to the guy who designed it!</p><p>The cheap, entry-level turntable market has plenty of options, but when you dig a little deeper, you'll find that most are all the same, kinda crappy units, reskinned, rebadged and repackaged. </p><p>Kanto set out to make its own turntable for the budget market that was built by them, not someone else. It uses an Audio Technica 3600L cartridge and costs $199, meaning newcomers to vinyl can grab a turntable and a budget speaker, like <a href="https://www.amazon.com/Kanto-YU-Bookshelf-Bluetooth-Detection/dp/B084YZKVTB/" target="_blank" rel="nofollow">Kanto's $349 YU</a>, for around $550. Not bad. </p><h2 id="4-ruark-r710">4. Ruark R710</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9600px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7bFLBVGBoiWdKfvjwAkLp5" name="DSC08347" alt="Ruark R710" src="https://cdn.mos.cms.futurecdn.net/7bFLBVGBoiWdKfvjwAkLp5.jpg" mos="" align="middle" fullscreen="" width="9600" height="5400" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>"The world is crying out for CD players," said Ruark's Design Manager, Jan Paoli, during the launch of the Ruark R710 media console at High End. </p><p>British high-end (but attainable) hi-fi manufacturer Ruark has been making radios and CD players for a long time, which is rather fortuitous given the (re)rising popularity of CDs right now.</p><p>The R710 is a music console, delivering 200W of power per channel and offering a wealth of connectivity options, including wireless stmreaing via Spotify Connect, Tidal Connect and Qobuz connect. </p><p>It offers hi-res streaming up to 32-bit/192kHz, features a phono stage for hooking up turntables, and is designed to be used with the new Ruark Talisman-R floor speakers — which I was treated to a demo of, and which sound utterly lovely, with a warm-but-detailed hi-fi sound.</p><p>The R710 will go on sale in September at £2,200, while the Talisman-R speakers will follow shortly afterwards in October, priced at £1,500. They'll be available in the U.S., although pricing has yet to be confirmed.</p><h2 id="5-ifi-gr2">5. iFi GR2</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:9984px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NkigHWNHdcyr86fia2abBa" name="DSC08410" alt="iFi GR2" src="https://cdn.mos.cms.futurecdn.net/NkigHWNHdcyr86fia2abBa.jpg" mos="" align="middle" fullscreen="" width="9984" height="5616" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>"A Hip-Dac 3 on steroids." That's how iFi's engineers described the new iFi GR2, and it's spot on. I own an iFi Hip-Dac 3, and love it, but the GR2 is a whole different beast. </p><p>Delivering 1,513mW of power, or 2,173mW in its max power mode, the GR2 is intended to be a pocketable powerhouse for hard-to-drive headphones.</p><p>Admittedly, it probably won't power the Meze ARTA above, but otherwise there aren't many headphones it can't drive, according to the engineers.</p><p>The GR2 takes what I love about the Hip-Dac 3 — its portability and simplicity of use — beefs it up with a touch screen, and adds more power. </p><p>In addition to iFi's Xbass mode, the GR2 also has an Xspace mode, which increases the width and spaciousness of sound. It worked extremely well in the ultra-spatial "Antigravity" by Sohn.</p><p>The <a href="https://ifi-audio.com/products/idsd-gr-2?srsltid=AfmBOorXfZLbtxR5Il2mV2f9IrQGvOru7txkrRY6czz3Rx2AUFy82goa" target="_blank" rel="nofollow">GR2 will cost $529</a>, can be pre-ordered now, and is expected to start shipping in July 2026.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-W21yoe"></div>                            </div>                            <script src="https://kwizly.com/embed/W21yoe.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/earbuds/i-never-thought-cheap-wired-earbuds-could-sound-so-good-heres-how-the-usd39-dunu-titan-x-change-the-budget-hi-res-game">‘I never thought cheap wired earbuds could sound so good’: here's how the $39 Dunu Titan X change the budget hi-res game</a></li><li><a href="https://www.tomsguide.com/audio/headphones/shokz-opendots-2-review">I tested the new Shokz OpenDots 2 headphones for three weeks and they’re better value than the Bose Open Ultra Earbuds</a></li><li><a href="https://www.tomsguide.com/audio/headphones/jbl-live-780nc-review">I tested JBL’s new mid-range headphones — and they’re zingy, fresh, and fun (and finally have hi-res audio!)</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I ditched my $54 JBL Go 5 to listen to music on the $49 8BitDo Retro Cube 2 speaker — and 8BitDo does something JBL doesn't ]]></title>
                                                                                                <dc:content><![CDATA[ <p>It isn't often that I come across a product that makes me say, "Damn, I want to keep that on my desk forever." </p><p>That very thing happened last week, when I tested the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review">8BitDo Retro Cube 2</a>, one of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> around. I took one look at it and said, "Mine! Mine!" like the seagulls from Finding Nemo. </p><p>Up until that point, I was using the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> to blast my favorite tunes, but now, I'm not sure I want to go back to the JBL — for a few good reasons.</p><div class="product"><a data-dimension112="fee2a85c-f324-43a7-a809-4ac3e432cd80" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:892px;"><p class="vanilla-image-block" style="padding-top:116.82%;"><img id="r5zUUMfK2PzaubVDbQfHCF" name="8BitDo-RetroCube2-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/r5zUUMfK2PzaubVDbQfHCF.jpg" mos="" align="middle" fullscreen="" width="892" height="1042" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The 8BitDo Retro Cube 2 is the quirkiest speaker around, and that’s due to its NES-inspired design and D-pad-style controls. The speaker comes with a wireless charging dock with a slot to store the 2.4GHz dongle safely, and it boasts class-leading battery life of 30 hours. Crisp treble and a refined midrange make the speaker shine, though the bass could be more powerful.<a class="view-deal button" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow" data-dimension112="fee2a85c-f324-43a7-a809-4ac3e432cd80" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49">View Deal</a></p></div><p>JBL is arguably one of the biggest names in the audio sphere and when it comes to pure, full-bodied sound, the Go 5 upstages the Retro Cube 2. </p><p>8BitDo, on the other hand, is known for making retro-looking gaming and computing peripherals, and isn't a big player in the audio world. </p><p>So why do I still want to stick with the Retro Cube 2? Allow me to tell you why.</p><h2 id="a-10-10-nes-inspired-design">A 10/10 NES-inspired design</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="o2qE8emV5Evh444E2tQyq3" name="8bitDo_speaker_ 9.JPG" alt="8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/o2qE8emV5Evh444E2tQyq3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When it comes to design, the 8BitDo Retro Cube 2 is easily the winner. Sure, the JBL Go 5 looks sleek and attractive, and doesn't draw eyeballs, but does it have an NES-inspired design? D-pad-style controls? A charging dock that's good-looking <em>and </em>practical? No, but the Retro Cube 2 does.</p><div><blockquote><p>Sure, the JBL Go 5 looks sleek and attractive, but does it have an NES-inspired design? No, but the Retro Cube 2 does.</p></blockquote></div><p>The Retro Cube 2's design is inspired by the Nintendo Entertainment System (NES), and it comes in a combination of beige, red and black. It looks utterly fantastic, and it's the cutest, quirkiest speaker I've tested so far. If you have an <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review" target="_blank">8BitDo Retro Mechanical Keyboard</a>, there's no better speaker to complement it.</p><p>Unlike any other speaker I've used, it comes with a charging dock so you can keep the speaker powered at all times. What's more, the charging dock has a slot for the Retro Cube 2's 2.4GHz dongle so that you don't lose it — neat!</p><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4"></video></div><p>But perhaps my favorite stylistic choice on the Retro Cube 2 is its D-pad-like buttons to control playback. They give great tactile feedback when pressed, and given how much fun they are to press, it's like having a fidget toy and speaker all-in-one. </p><p>The best part? When you power on the speaker, it sings, “8BitDooooo.” Does the Go 5 do that? No way.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6938px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="fjPya8gWyNVCdRvgFLQGsh" name="8BitDo-Retro-Cube-2" alt="8BitDo Retro Cube 2 and 8BitDo Retro Mechanical Keyboard" src="https://cdn.mos.cms.futurecdn.net/fjPya8gWyNVCdRvgFLQGsh.jpg" mos="" align="middle" fullscreen="" width="6938" height="3903" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Go 5 naturally has a couple of aces up its sleeve — waterproofing and a rugged design, as well as better Bluetooth connectivity and support for advanced codecs. But given just how lovely the Retro Cube 2 looks, I'm OK living without them. </p><h2 id="all-day-listening-yes-please">All-day listening? Yes, please</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DrpQxnFVp8Lu8caQsiQUy3" name="8bitDo_speaker_ 4.JPG" alt="8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/DrpQxnFVp8Lu8caQsiQUy3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of the key differences between the 8BitDo Retro Cube 2 and the JBL Go 5 is the battery life — and truth be told, it isn't even close. </p><p>The Go 5 is rated for just 10 hours, while the Retro Cube 2 achieves thrice that. At 30 hours, the Retro Cube 2 lasts a long, long time, and as it comes with a charging dock, you can keep it powered on at all times.</p><p>Question is, would you <em>want</em> to listen to music through the Retro Cube 2 all day long? Yes and no. Given that JBL dominates the audio space, it's not surprising that it sounds better than the Retro Cube 2 — but that doesn't mean the Retro Cube 2 is <em>all </em>bad. Treble is crisp and shiny without overpowering the rest of the soundstage, while the midrange is refined and tuned to make songs sound spacious.</p><p>The only real complaint I have with the Retro Cube 2's sound profile is the bass, which sounds muted. It's true that small speakers struggle to reproduce powerful bass, but the Go 5 does just that. The bass packs plenty of low-end impact on the Go 5, which feels non-existent on the Retro Cube 2. And as someone whose favorite bit of a song is the bass, the Retro Cube 2 left me wanting more. </p><p>You can read my in-depth sound quality analysis in my full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review">8BitDo Retro Cube 2 review</a>.</p><h2 id="the-price-is-right">The price is right</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bg7RCSuhiL75T76EpeJde3" name="8bitDo_speaker_ 1.JPG" alt="8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/Bg7RCSuhiL75T76EpeJde3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Given everything I've said so far, I think it's pretty clear that I adore the 8BitDo Retro Cube 2. It looks fantastic, it sounds almost great, and it can comfortably last an all-day listening session. In fact, because of how beautiful it looks, the Retro Cube 2 has earned a permanent spot on my desk. Unlike most products that I promptly box up when I'm done testing them, the Retro Cube 2 is here to stay.</p><p>And the price is just right. The Retro Cube 2 is available for just <a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">£39 at Amazon</a>, making it an affordable and attractive speaker-cum-centerpiece package. As I write this, I'm listening to Lana Del Rey on the Retro Cube 2, and playing with those outstanding D-pad buttons. </p><p>Don't know about you, but I love my 🎶 8BitDooooo 🎶.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Odk2ZW"></div>                            </div>                            <script src="https://kwizly.com/embed/Odk2ZW.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review">I’ve never seen a speaker as cute and quirky as the NES-inspired 8BitDo Retro Cube 2 — and it boasts surprisingly good sound for just $49</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-versus-jbl-go-4-face-off">I tested the JBL Go 5 vs JBL Go 4 budget Bluetooth speakers — and there’s a clear winner under $60</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/im-soundtracking-my-summer-with-the-usd54-jbl-go-5-its-one-of-the-best-budget-speakers-ive-ever-used">I’m soundtracking my summer with the $54 JBL Go 5 — it’s one of the best budget speakers I’ve ever used</a></li></ul><div data-widget-type="multimodelreview" data-model-name="8BitDo Retro Cube 2,JBL Go 5"></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/i-ditched-my-usd54-jbl-go-5-to-listen-to-music-on-the-usd49-8bitdo-retro-cube-2-speaker</link>
                                                                            <description>
                            <![CDATA[ I've been testing the 8BitDo Retro Cube 2 speaker and though it doesn't sound as full-bodied as the JBL Go 5, its cute looks and long battery life have won me over. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">PCFPgk6gdVziF4qUvtouDR</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/7SRu3iTvSprF9Aa6eM77HR-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 06 Jun 2026 05:00:00 +0000</pubDate>                                                                                                                                <updated>Wed, 10 Jun 2026 13:54:41 +0000</updated>
                                                                                                                                            <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/7SRu3iTvSprF9Aa6eM77HR-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker]]></media:description>                                                            <media:text><![CDATA[A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker]]></media:text>
                                <media:title type="plain"><![CDATA[A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/7SRu3iTvSprF9Aa6eM77HR-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>It isn't often that I come across a product that makes me say, "Damn, I want to keep that on my desk forever." </p><p>That very thing happened last week, when I tested the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review">8BitDo Retro Cube 2</a>, one of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> around. I took one look at it and said, "Mine! Mine!" like the seagulls from Finding Nemo. </p><p>Up until that point, I was using the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5</a> to blast my favorite tunes, but now, I'm not sure I want to go back to the JBL — for a few good reasons.</p><div class="product"><a data-dimension112="fee2a85c-f324-43a7-a809-4ac3e432cd80" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:892px;"><p class="vanilla-image-block" style="padding-top:116.82%;"><img id="r5zUUMfK2PzaubVDbQfHCF" name="8BitDo-RetroCube2-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/r5zUUMfK2PzaubVDbQfHCF.jpg" mos="" align="middle" fullscreen="" width="892" height="1042" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The 8BitDo Retro Cube 2 is the quirkiest speaker around, and that’s due to its NES-inspired design and D-pad-style controls. The speaker comes with a wireless charging dock with a slot to store the 2.4GHz dongle safely, and it boasts class-leading battery life of 30 hours. Crisp treble and a refined midrange make the speaker shine, though the bass could be more powerful.<a class="view-deal button" href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow" data-dimension112="fee2a85c-f324-43a7-a809-4ac3e432cd80" data-action="Deal Block" data-label="Retro Cube 2" data-dimension48="Retro Cube 2" data-dimension25="$49">View Deal</a></p></div><p>JBL is arguably one of the biggest names in the audio sphere and when it comes to pure, full-bodied sound, the Go 5 upstages the Retro Cube 2. </p><p>8BitDo, on the other hand, is known for making retro-looking gaming and computing peripherals, and isn't a big player in the audio world. </p><p>So why do I still want to stick with the Retro Cube 2? Allow me to tell you why.</p><h2 id="a-10-10-nes-inspired-design">A 10/10 NES-inspired design</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="o2qE8emV5Evh444E2tQyq3" name="8bitDo_speaker_ 9.JPG" alt="8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/o2qE8emV5Evh444E2tQyq3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>When it comes to design, the 8BitDo Retro Cube 2 is easily the winner. Sure, the JBL Go 5 looks sleek and attractive, and doesn't draw eyeballs, but does it have an NES-inspired design? D-pad-style controls? A charging dock that's good-looking <em>and </em>practical? No, but the Retro Cube 2 does.</p><div><blockquote><p>Sure, the JBL Go 5 looks sleek and attractive, but does it have an NES-inspired design? No, but the Retro Cube 2 does.</p></blockquote></div><p>The Retro Cube 2's design is inspired by the Nintendo Entertainment System (NES), and it comes in a combination of beige, red and black. It looks utterly fantastic, and it's the cutest, quirkiest speaker I've tested so far. If you have an <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review" target="_blank">8BitDo Retro Mechanical Keyboard</a>, there's no better speaker to complement it.</p><p>Unlike any other speaker I've used, it comes with a charging dock so you can keep the speaker powered at all times. What's more, the charging dock has a slot for the Retro Cube 2's 2.4GHz dongle so that you don't lose it — neat!</p><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4"></video></div><p>But perhaps my favorite stylistic choice on the Retro Cube 2 is its D-pad-like buttons to control playback. They give great tactile feedback when pressed, and given how much fun they are to press, it's like having a fidget toy and speaker all-in-one. </p><p>The best part? When you power on the speaker, it sings, “8BitDooooo.” Does the Go 5 do that? No way.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6938px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="fjPya8gWyNVCdRvgFLQGsh" name="8BitDo-Retro-Cube-2" alt="8BitDo Retro Cube 2 and 8BitDo Retro Mechanical Keyboard" src="https://cdn.mos.cms.futurecdn.net/fjPya8gWyNVCdRvgFLQGsh.jpg" mos="" align="middle" fullscreen="" width="6938" height="3903" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Go 5 naturally has a couple of aces up its sleeve — waterproofing and a rugged design, as well as better Bluetooth connectivity and support for advanced codecs. But given just how lovely the Retro Cube 2 looks, I'm OK living without them. </p><h2 id="all-day-listening-yes-please">All-day listening? Yes, please</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DrpQxnFVp8Lu8caQsiQUy3" name="8bitDo_speaker_ 4.JPG" alt="8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/DrpQxnFVp8Lu8caQsiQUy3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of the key differences between the 8BitDo Retro Cube 2 and the JBL Go 5 is the battery life — and truth be told, it isn't even close. </p><p>The Go 5 is rated for just 10 hours, while the Retro Cube 2 achieves thrice that. At 30 hours, the Retro Cube 2 lasts a long, long time, and as it comes with a charging dock, you can keep it powered on at all times.</p><p>Question is, would you <em>want</em> to listen to music through the Retro Cube 2 all day long? Yes and no. Given that JBL dominates the audio space, it's not surprising that it sounds better than the Retro Cube 2 — but that doesn't mean the Retro Cube 2 is <em>all </em>bad. Treble is crisp and shiny without overpowering the rest of the soundstage, while the midrange is refined and tuned to make songs sound spacious.</p><p>The only real complaint I have with the Retro Cube 2's sound profile is the bass, which sounds muted. It's true that small speakers struggle to reproduce powerful bass, but the Go 5 does just that. The bass packs plenty of low-end impact on the Go 5, which feels non-existent on the Retro Cube 2. And as someone whose favorite bit of a song is the bass, the Retro Cube 2 left me wanting more. </p><p>You can read my in-depth sound quality analysis in my full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review">8BitDo Retro Cube 2 review</a>.</p><h2 id="the-price-is-right">The price is right</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bg7RCSuhiL75T76EpeJde3" name="8bitDo_speaker_ 1.JPG" alt="8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/Bg7RCSuhiL75T76EpeJde3.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Given everything I've said so far, I think it's pretty clear that I adore the 8BitDo Retro Cube 2. It looks fantastic, it sounds almost great, and it can comfortably last an all-day listening session. In fact, because of how beautiful it looks, the Retro Cube 2 has earned a permanent spot on my desk. Unlike most products that I promptly box up when I'm done testing them, the Retro Cube 2 is here to stay.</p><p>And the price is just right. The Retro Cube 2 is available for just <a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">£39 at Amazon</a>, making it an affordable and attractive speaker-cum-centerpiece package. As I write this, I'm listening to Lana Del Rey on the Retro Cube 2, and playing with those outstanding D-pad buttons. </p><p>Don't know about you, but I love my 🎶 8BitDooooo 🎶.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Odk2ZW"></div>                            </div>                            <script src="https://kwizly.com/embed/Odk2ZW.js" async></script><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review">I’ve never seen a speaker as cute and quirky as the NES-inspired 8BitDo Retro Cube 2 — and it boasts surprisingly good sound for just $49</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-versus-jbl-go-4-face-off">I tested the JBL Go 5 vs JBL Go 4 budget Bluetooth speakers — and there’s a clear winner under $60</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/im-soundtracking-my-summer-with-the-usd54-jbl-go-5-its-one-of-the-best-budget-speakers-ive-ever-used">I’m soundtracking my summer with the $54 JBL Go 5 — it’s one of the best budget speakers I’ve ever used</a></li></ul><div data-widget-type="multimodelreview" data-model-name="8BitDo Retro Cube 2,JBL Go 5"></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Sony Bravia Theatre Bar 6 review ]]></title>
                                                                                                <dc:content><![CDATA[ <div  class="fancy-box"><div class="fancy_box-title">Sony Bravia Theatre Bar 6 review: Specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price: </strong>$699 / £449<br><strong>Connectivity: </strong>HDMI eARC, Bluetooth<br><strong>Weight: </strong>33 lbs<br><strong>Dimensions: </strong>37.5 x 2.6 inches x 4.4 inches (Soundbar); 8.31 x 15.31 x 16.94 inches (Subwoofer)<br><strong>Colors: </strong>Black<br><strong>Subwoofer: </strong>Yes<br><strong>Dolby Atmos: </strong>Yes<br><strong>Channel: </strong>3.1.2</p></div></div><p>The world of  soundbars has gotten a whole lot better over the past few years. The likes of the <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2"><u>Sonos Beam Gen 2</u></a> have made it easier to get Dolby Atmos and a reasonably vibrant sound without having to spend thousands of dollars. Sony's take on this is the Sony Bravia Theatre Bar 6.</p><p>The Theatre Bar 6 is designed for those who want to cut out the fuss of improving their living room audio. Sure, it doesn't have fancy features like rear speakers, but it still comes with a wireless subwoofer for improved bass, and you can often find it for less than $600. That said, it's lacking some features that some of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars"><u>best soundbars</u></a> offer. </p><p>Is the Sony Bravia Theatre Bar 6 worth buying, or should you look elsewhere for Atmos? I tested the Bravia Theater 6 Bar to find out.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-cheat-sheet"><span>Sony Bravia Theatre Bar 6 review: Cheat Sheet</span></h3><ul><li>What is it? A compact Dolby Atmos soundbar that comes with a wireless subwoofer</li><li>Who is it for? Folks who want to cut out the fuss of improving their living room audio.</li><li>What does it cost? $699 / £449</li><li>What do we like? Its sleek design and inclusion of a wireless subwoofer</li><li>What don’t we like? The Bar 6 leaves a lot to be desired in the low end and it’s missing wireless features like AirPlay 2.</li></ul><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-price-and-availability"><span>Sony Bravia Theatre Bar 6 review: Price and availability</span></h3><p>Sony offers the Bravia Theatre Bar 6 at $699 or £449, which puts it squarely in mid-range territory — comfortably above the budget options, but also far below the high-end alternatives, including some of the options in Sony's lineup.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-design"><span>Sony Bravia Theatre Bar 6 review: Design</span></h3><p>As a mid-range soundbar, the Sony Bravia Theatre Bar 6 is pretty no-fuss in terms of design and setup. It’s a sleek black bar that measures 37.5 inches wide, 2.6 inches high, and 4.4 inches deep. That's bigger than some of the ultra-compact soundbar models out there, but also more compact than larger models like the <a href="https://www.tomsguide.com/audio/soundbars/sonos-arc-ultra-review"><u>Sonos Arc Ultra</u></a>. It was able to fit between the legs of a 65-inch TV that I tested it with, but only just, and if your TV is smaller, or even if you have a model with narrower leg placement, you might need to move the soundbar in front of those legs instead of between them. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Q5ReeJFpjty4bpcvFk38ig" name="Sony Bravia Theatre Bar 6--4" alt="Sony Bravia Theatre Bar 6 on a TV stand" src="https://cdn.mos.cms.futurecdn.net/Q5ReeJFpjty4bpcvFk38ig.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Sony actually includes a set of detachable feet that are designed to sit on the legs of some of its Bravia TVs, which is a nice touch for those in Sony's ecosystem.</p><p>On the top of the soundbar are small windows for the up-firing speakers, while the rest of the speakers' drivers fire out towards the front and sides. The front corners are rounded while the back corners are sharper, which is a nice design touch. On the back you'll find HDMI and optical ports, but that's it, apart from a power input. There’s only one HDMI port — so no HDMI passthrough.</p><p>The subwoofer offers a similar build quality and overall look to the soundbar, made from the same materials and in the same color. It is a little larger than the subwoofer that comes with some soundbars, but not overly so, measuring 8.31inches wide, 15.31 inches high, and 16.94 inches deep. You'll want to think about where you want to place it in your living room, though.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Xfos3zBtE9kMx8UXvnb3hg" name="Sony Bravia Theatre Bar 6--5" alt="The remote for a Sony Bravia Theatre Bar 6" src="https://cdn.mos.cms.futurecdn.net/Xfos3zBtE9kMx8UXvnb3hg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Last but not least is the remote, which is very simple. It's small and offers controls for power, input, volume, bass volume, night mode, and mute. If you connect your soundbar through HDMI eARC, you probably won't need to use the included remote all that much, as you'll be able to change volume using your TV's remote. But if you do prefer a separate remote for the soundbar, you'll get one, which is nice.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-setup"><span>Sony Bravia Theatre Bar 6 review: Setup</span></h3><p>Setting everything up is incredibly easy. All you do is plug the power in and connect the HDMI port to your TV's HDMI eARC port. If you're using a compatible Sony Bravia TV, it'll detect and connect to it right away, but even if not, when connected through HDMI eARC, your TV should know to output audio to the soundbar.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wkmQZ6CuzUSZJPisGwzRcg" name="Sony Bravia Theatre Bar 6--2" alt="The Sony Bravia Theatre Bar 6 wireless subwoofer" src="https://cdn.mos.cms.futurecdn.net/wkmQZ6CuzUSZJPisGwzRcg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Setting up the subwoofer is very easy too. All you have to do is plug it in and the soundbar should automatically connect to it without you actually having to do anything. </p><p>Once you've connected the soundbar to your TV, you'll want to connect it to the app, and this is very easy as well. You don't even actually have to create an account if you don't want to, though doing so will give you additional controls and features. After you download the Sony Bravia Connect app, you'll be walked through connecting the soundbar to the app through Bluetooth, which took me under one minute, as the app can automatically detect the soundbar and initiate a connection.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-connectivity-and-controls"><span>Sony Bravia Theatre Bar 6 review: Connectivity and controls</span></h3><p>One area where the Theatre Bar 6 doesn't quite match up to the competition is in connectivity. Of course, the main way to connect the soundbar is through HDMI eARC — which is how most soundbars connect to TVs these days. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jtAsugnoLLULkXiDW3STcg" name="Sony Bravia Theatre Bar 6--3" alt="Sony Bravia Theatre Bar 6 on a TV stand" src="https://cdn.mos.cms.futurecdn.net/jtAsugnoLLULkXiDW3STcg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Where it's lacking is in <em>additional</em> connectivity. There's no 3.5mm port for connecting something like a phone through a wired connection. Plus, if you want to connect your phone wirelessly, you're stuck with Bluetooth 5.3 and not a Wi-Fi-based standard like Apple's AirPlay 2 or Google Cast. </p><p>Wi-Fi-based connectivity is an area where budget soundbars often compromise, but there are plenty of options in this price range like the Sonos Beam Gen 2, which does have more advanced wireless connectivity for those who want to integrate their soundbar with a multi-room setup. In other words, if you're looking for a soundbar that will work with other speakers for whole-home audio, the Sony Bravia Theatre Bar 6 probably isn't the soundbar for you.</p><p>The app in general is pretty bare bones, and that means that you won't get a ton of customization options. You will get controls for things like standby modes and DSEE, but you won't get an EQ that you can use to more finely tune the audio. That's a little disappointing.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-performance"><span>Sony Bravia Theatre Bar 6 review: Performance</span></h3><p>The Sony Bravia Theatre Bar 6 offers a 3.1.2 channel configuration, which includes three front-firing drivers coupled with two up-firing drivers and a wireless subwoofer for the low end. Those up-firing drivers allow it to support Dolby Atmos and DTS:X, which isn't necessarily a given in this price range, but also not uncommon.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hC8bxnZ2FTnLN8WZYcwWgg" name="Sony Bravia Theatre Bar 6--6" alt="Close up of the speaker on a Sony Bravia Theatre Bar 6" src="https://cdn.mos.cms.futurecdn.net/hC8bxnZ2FTnLN8WZYcwWgg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I found the overall audio response to be quite good. The drivers and their placement made for an immersive sound overall, including for both music and movies. The soundstage was decently wide when listening to music, and while you can certainly get more immersion from systems with rear speakers, I found the height effect on offer by the Theatre Bar 6 to be better than expected for a mid-range soundbar. It helped make the sound effects in <em>Avengers: Endgame</em> more immersive than they otherwise would be, especially when it came to some of the higher-frequency effects during the final fight scene.</p><p>Now, to be clear, the height effect that you'll get on the Theatre Bar 6 isn't perfect — and it's not necessarily ultra-convincing when it comes to hearing specific elements in the mix, but you do at least get some immersion from the soundbar when watching movies in Dolby Atmos. In other words, while it definitely adds a sense of height to the mix, it doesn’t quite separate out those elements in a way that might allow you to pinpoint exactly what you’re hearing in those height channels – especially in dense mixes, like the aforementioned final scene in <em>Avengers: Endgame</em>. </p><p>The subwoofer definitely helps allow the system to produce deep frequencies compared to systems that don't come with a subwoofer, but I wouldn't say it's the most powerful subwoofer out there, despite its somewhat large size. It can get reasonably loud, but its frequency range is a little limited, meaning that it can sound a little muddy and overly boosted in the low mids and upper bass rather than extending deep into the bass spectrum. That’s especially true in already bass-heavy tracks, like Kendrick Lamar’s <em>Loyalty</em>, in which the bass somewhat overpowered other elements in the track.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-verdict"><span>Sony Bravia Theatre Bar 6 review: Verdict</span></h3><p>The Sony Bravia Theatre Bar 6 is a well-built, decently compact soundbar with a solid audio response overall. It offers good spatial audio for the price, and it's nice that it comes with a subwoofer at all, when plenty of others at this price point don't. That said, the subwoofer leaves a lot to be desired in the low end, and the high frequencies aren't as crisp as I would have liked. On top of that, the soundbar is missing wireless features like AirPlay 2, limiting how much it can integrate with the rest of your speaker ecosystem.</p><p>Those things don't mean it's not worth consideration — it just depends what your priorities are. If you want a no-fuss soundbar and don't care much about getting something with Wi-Fi connectivity that can support, then the Bravia Theatre Bar 6 is actually a pretty compelling option. But, if you want more in the way of features, and a slightly better-rounded audio response, then something like the Sonos Beam Gen 2 is worth going for instead — especially as it allows you to add a subwoofer down the line anyway.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/sony-bravia-theatre-bar-6-review</link>
                                                                            <description>
                            <![CDATA[ The Sony Bravia Theatre Bar 6 is a good-sounding midrange soundbar that supports Dolby Atmos and comes with a wireless subwoofer. However, its performance and features aren’t quite up to the level as comparable systems. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">JPJ9sact2yGAeqTJbMQRga</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/jtAsugnoLLULkXiDW3STcg-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 02 Jun 2026 12:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                                    <dc:creator><![CDATA[ Christian de Looper ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/cyHuThvGLCKTfcMnWJiY4Y.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Christian de Looper is a freelance writer who has covered every facet of consumer tech, including mobile, audio, home theater, computing, gaming, and even car tech. At Tom’s Guide, Christian covers TV and home theater tech, and has reviewed dozens of TVs, soundbars, and A/V receivers, including those from the likes of Samsung, Hisense, TCL, and Vizio. Christian has also covered numerous industry events, from CES to F8, and is an expert at writing reviews, buying guides, and news. Outside of Tom’s Guide, Christian’s byline can be seen at TechRadar, Forbes, Digital Trends, Business Insider, and more. Follow Christian on Twitter to keep up with all his tech coverage.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/jtAsugnoLLULkXiDW3STcg-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Sony Bravia Theatre Bar 6 on a TV stand]]></media:description>                                                            <media:text><![CDATA[Sony Bravia Theatre Bar 6 on a TV stand]]></media:text>
                                <media:title type="plain"><![CDATA[Sony Bravia Theatre Bar 6 on a TV stand]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/jtAsugnoLLULkXiDW3STcg-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <div  class="fancy-box"><div class="fancy_box-title">Sony Bravia Theatre Bar 6 review: Specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price: </strong>$699 / £449<br><strong>Connectivity: </strong>HDMI eARC, Bluetooth<br><strong>Weight: </strong>33 lbs<br><strong>Dimensions: </strong>37.5 x 2.6 inches x 4.4 inches (Soundbar); 8.31 x 15.31 x 16.94 inches (Subwoofer)<br><strong>Colors: </strong>Black<br><strong>Subwoofer: </strong>Yes<br><strong>Dolby Atmos: </strong>Yes<br><strong>Channel: </strong>3.1.2</p></div></div><p>The world of  soundbars has gotten a whole lot better over the past few years. The likes of the <a href="https://www.tomsguide.com/reviews/sonos-beam-gen-2"><u>Sonos Beam Gen 2</u></a> have made it easier to get Dolby Atmos and a reasonably vibrant sound without having to spend thousands of dollars. Sony's take on this is the Sony Bravia Theatre Bar 6.</p><p>The Theatre Bar 6 is designed for those who want to cut out the fuss of improving their living room audio. Sure, it doesn't have fancy features like rear speakers, but it still comes with a wireless subwoofer for improved bass, and you can often find it for less than $600. That said, it's lacking some features that some of the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars"><u>best soundbars</u></a> offer. </p><p>Is the Sony Bravia Theatre Bar 6 worth buying, or should you look elsewhere for Atmos? I tested the Bravia Theater 6 Bar to find out.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-cheat-sheet"><span>Sony Bravia Theatre Bar 6 review: Cheat Sheet</span></h3><ul><li>What is it? A compact Dolby Atmos soundbar that comes with a wireless subwoofer</li><li>Who is it for? Folks who want to cut out the fuss of improving their living room audio.</li><li>What does it cost? $699 / £449</li><li>What do we like? Its sleek design and inclusion of a wireless subwoofer</li><li>What don’t we like? The Bar 6 leaves a lot to be desired in the low end and it’s missing wireless features like AirPlay 2.</li></ul><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-price-and-availability"><span>Sony Bravia Theatre Bar 6 review: Price and availability</span></h3><p>Sony offers the Bravia Theatre Bar 6 at $699 or £449, which puts it squarely in mid-range territory — comfortably above the budget options, but also far below the high-end alternatives, including some of the options in Sony's lineup.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-design"><span>Sony Bravia Theatre Bar 6 review: Design</span></h3><p>As a mid-range soundbar, the Sony Bravia Theatre Bar 6 is pretty no-fuss in terms of design and setup. It’s a sleek black bar that measures 37.5 inches wide, 2.6 inches high, and 4.4 inches deep. That's bigger than some of the ultra-compact soundbar models out there, but also more compact than larger models like the <a href="https://www.tomsguide.com/audio/soundbars/sonos-arc-ultra-review"><u>Sonos Arc Ultra</u></a>. It was able to fit between the legs of a 65-inch TV that I tested it with, but only just, and if your TV is smaller, or even if you have a model with narrower leg placement, you might need to move the soundbar in front of those legs instead of between them. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Q5ReeJFpjty4bpcvFk38ig" name="Sony Bravia Theatre Bar 6--4" alt="Sony Bravia Theatre Bar 6 on a TV stand" src="https://cdn.mos.cms.futurecdn.net/Q5ReeJFpjty4bpcvFk38ig.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Sony actually includes a set of detachable feet that are designed to sit on the legs of some of its Bravia TVs, which is a nice touch for those in Sony's ecosystem.</p><p>On the top of the soundbar are small windows for the up-firing speakers, while the rest of the speakers' drivers fire out towards the front and sides. The front corners are rounded while the back corners are sharper, which is a nice design touch. On the back you'll find HDMI and optical ports, but that's it, apart from a power input. There’s only one HDMI port — so no HDMI passthrough.</p><p>The subwoofer offers a similar build quality and overall look to the soundbar, made from the same materials and in the same color. It is a little larger than the subwoofer that comes with some soundbars, but not overly so, measuring 8.31inches wide, 15.31 inches high, and 16.94 inches deep. You'll want to think about where you want to place it in your living room, though.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Xfos3zBtE9kMx8UXvnb3hg" name="Sony Bravia Theatre Bar 6--5" alt="The remote for a Sony Bravia Theatre Bar 6" src="https://cdn.mos.cms.futurecdn.net/Xfos3zBtE9kMx8UXvnb3hg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Last but not least is the remote, which is very simple. It's small and offers controls for power, input, volume, bass volume, night mode, and mute. If you connect your soundbar through HDMI eARC, you probably won't need to use the included remote all that much, as you'll be able to change volume using your TV's remote. But if you do prefer a separate remote for the soundbar, you'll get one, which is nice.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-setup"><span>Sony Bravia Theatre Bar 6 review: Setup</span></h3><p>Setting everything up is incredibly easy. All you do is plug the power in and connect the HDMI port to your TV's HDMI eARC port. If you're using a compatible Sony Bravia TV, it'll detect and connect to it right away, but even if not, when connected through HDMI eARC, your TV should know to output audio to the soundbar.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wkmQZ6CuzUSZJPisGwzRcg" name="Sony Bravia Theatre Bar 6--2" alt="The Sony Bravia Theatre Bar 6 wireless subwoofer" src="https://cdn.mos.cms.futurecdn.net/wkmQZ6CuzUSZJPisGwzRcg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Setting up the subwoofer is very easy too. All you have to do is plug it in and the soundbar should automatically connect to it without you actually having to do anything. </p><p>Once you've connected the soundbar to your TV, you'll want to connect it to the app, and this is very easy as well. You don't even actually have to create an account if you don't want to, though doing so will give you additional controls and features. After you download the Sony Bravia Connect app, you'll be walked through connecting the soundbar to the app through Bluetooth, which took me under one minute, as the app can automatically detect the soundbar and initiate a connection.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-connectivity-and-controls"><span>Sony Bravia Theatre Bar 6 review: Connectivity and controls</span></h3><p>One area where the Theatre Bar 6 doesn't quite match up to the competition is in connectivity. Of course, the main way to connect the soundbar is through HDMI eARC — which is how most soundbars connect to TVs these days. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jtAsugnoLLULkXiDW3STcg" name="Sony Bravia Theatre Bar 6--3" alt="Sony Bravia Theatre Bar 6 on a TV stand" src="https://cdn.mos.cms.futurecdn.net/jtAsugnoLLULkXiDW3STcg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Where it's lacking is in <em>additional</em> connectivity. There's no 3.5mm port for connecting something like a phone through a wired connection. Plus, if you want to connect your phone wirelessly, you're stuck with Bluetooth 5.3 and not a Wi-Fi-based standard like Apple's AirPlay 2 or Google Cast. </p><p>Wi-Fi-based connectivity is an area where budget soundbars often compromise, but there are plenty of options in this price range like the Sonos Beam Gen 2, which does have more advanced wireless connectivity for those who want to integrate their soundbar with a multi-room setup. In other words, if you're looking for a soundbar that will work with other speakers for whole-home audio, the Sony Bravia Theatre Bar 6 probably isn't the soundbar for you.</p><p>The app in general is pretty bare bones, and that means that you won't get a ton of customization options. You will get controls for things like standby modes and DSEE, but you won't get an EQ that you can use to more finely tune the audio. That's a little disappointing.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-performance"><span>Sony Bravia Theatre Bar 6 review: Performance</span></h3><p>The Sony Bravia Theatre Bar 6 offers a 3.1.2 channel configuration, which includes three front-firing drivers coupled with two up-firing drivers and a wireless subwoofer for the low end. Those up-firing drivers allow it to support Dolby Atmos and DTS:X, which isn't necessarily a given in this price range, but also not uncommon.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hC8bxnZ2FTnLN8WZYcwWgg" name="Sony Bravia Theatre Bar 6--6" alt="Close up of the speaker on a Sony Bravia Theatre Bar 6" src="https://cdn.mos.cms.futurecdn.net/hC8bxnZ2FTnLN8WZYcwWgg.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I found the overall audio response to be quite good. The drivers and their placement made for an immersive sound overall, including for both music and movies. The soundstage was decently wide when listening to music, and while you can certainly get more immersion from systems with rear speakers, I found the height effect on offer by the Theatre Bar 6 to be better than expected for a mid-range soundbar. It helped make the sound effects in <em>Avengers: Endgame</em> more immersive than they otherwise would be, especially when it came to some of the higher-frequency effects during the final fight scene.</p><p>Now, to be clear, the height effect that you'll get on the Theatre Bar 6 isn't perfect — and it's not necessarily ultra-convincing when it comes to hearing specific elements in the mix, but you do at least get some immersion from the soundbar when watching movies in Dolby Atmos. In other words, while it definitely adds a sense of height to the mix, it doesn’t quite separate out those elements in a way that might allow you to pinpoint exactly what you’re hearing in those height channels – especially in dense mixes, like the aforementioned final scene in <em>Avengers: Endgame</em>. </p><p>The subwoofer definitely helps allow the system to produce deep frequencies compared to systems that don't come with a subwoofer, but I wouldn't say it's the most powerful subwoofer out there, despite its somewhat large size. It can get reasonably loud, but its frequency range is a little limited, meaning that it can sound a little muddy and overly boosted in the low mids and upper bass rather than extending deep into the bass spectrum. That’s especially true in already bass-heavy tracks, like Kendrick Lamar’s <em>Loyalty</em>, in which the bass somewhat overpowered other elements in the track.</p><h3 class="article-body__section" id="section-sony-bravia-theatre-bar-6-review-verdict"><span>Sony Bravia Theatre Bar 6 review: Verdict</span></h3><p>The Sony Bravia Theatre Bar 6 is a well-built, decently compact soundbar with a solid audio response overall. It offers good spatial audio for the price, and it's nice that it comes with a subwoofer at all, when plenty of others at this price point don't. That said, the subwoofer leaves a lot to be desired in the low end, and the high frequencies aren't as crisp as I would have liked. On top of that, the soundbar is missing wireless features like AirPlay 2, limiting how much it can integrate with the rest of your speaker ecosystem.</p><p>Those things don't mean it's not worth consideration — it just depends what your priorities are. If you want a no-fuss soundbar and don't care much about getting something with Wi-Fi connectivity that can support, then the Bravia Theatre Bar 6 is actually a pretty compelling option. But, if you want more in the way of features, and a slightly better-rounded audio response, then something like the Sonos Beam Gen 2 is worth going for instead — especially as it allows you to add a subwoofer down the line anyway.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I’ve never seen a speaker as cute and quirky as the NES-inspired 8BitDo Retro Cube 2 — and it boasts surprisingly good sound for just $49 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’ve tested many of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> in my time, but most of them look either the same or they sport bland or uninventive design. Not the 8BitDo Retro Cube 2. It’s the cutest, quirkiest speaker you can buy today, and not for a lot of money as it costs just $49. An NES-inspired design and D-pad-style buttons make the Retro Cube 2 a joy to look at and use.</p><p>In addition to its simply fantastic design, the Retro Cube 2 boasts long battery life with up to 30 hours of playback time. It comes with a practical wireless charging dock too. Sound quality is great, with treble that shines and strong dynamics. However, given the speaker’s size and limited frequency response, the bass is a little lacking. It isn’t waterproof either, and its underbaked companion app doesn’t do it any favors.</p><p>Should you still buy it? Read my full 8BitDo Retro Cube 2 review to find out.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-specs"><span>8BitDo Retro Cube 2 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">£39</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>No IP rating</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3 with SBC, 2.4GHz, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>5W x 2</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>120Hz-15KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>30 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>3.47 x 3.45 x 3.45 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>15.87oz (speaker); 3.06oz (charging dock)</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>N Edition (beige, red and black)</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-price-availability"><span>8BitDo Retro Cube 2 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eRckzAjnhpaftZk3Aif5uQ" name="8bitDo_speaker_ 4.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/eRckzAjnhpaftZk3Aif5uQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The 8BitDo Retro Cube 2 is the successor to the first-gen speaker, which first came out in 2019. The 2026 iteration is priced similarly, and is available for <a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">£39 at Amazon</a>. It’s available in one color only, at the time of writing: N Edition, which is a combination of beige, red and black.</p><p>If it’s a budget Bluetooth speaker you’re after, the Retro Cube 2 is perfect for you. It’s cheaper than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5 </a>($54) and the Tribit Stormbox Micro 3 ($64). It’s a little pricier than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> ($39) and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tozo-pe1-review">Tozo PE1</a> ($39) but it’s still in the same ballpark. So, why should you buy the Retro Cube 2 from a brand that isn’t as well-known for audio gear as it is for its retro-style peripherals? The design and long battery life, for starters, and we’ll get into the details next.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-design"><span>8BitDo Retro Cube 2 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TFdpvvuvdeBc2KzxWgkD2R" name="8bitDo_speaker_ 3.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/TFdpvvuvdeBc2KzxWgkD2R.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ve tested a lot of Bluetooth speakers in my time, but I’ve never seen one as cute as the 8BitDo Retro Cube 2. Sporting a design inspired by the NES, the Retro Cube 2 fits right in with the brand’s other retro-style products. If you own the <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review">8BitDo Retro Mechanical Keyboard</a> like I do, or the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/8bitdo-64-review">8BitDo 64</a> N64-styled gamepad, the Retro Cube 2 is the perfect accompaniment, thanks to its beige, red and black colors.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6938px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="fjPya8gWyNVCdRvgFLQGsh" name="8BitDo-Retro-Cube-2" alt="8BitDo Retro Cube 2 and 8BitDo Retro Mechanical Keyboard" src="https://cdn.mos.cms.futurecdn.net/fjPya8gWyNVCdRvgFLQGsh.jpg" mos="" align="middle" fullscreen="" width="6938" height="3903" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Retro Cube 2 is, as the name suggests, a cube. Four red speaker grilles are located on each face, and the top of the cube hosts the D-pad physical buttons, which I’ll talk about shortly. With subtle branding, the Retro Cube 2 is minimalist and good-looking, and is sure to draw eyeballs (in a good way). There’s no flashy RGB lighting, which I like, and the only bits that light up are the setting indicators on the top.</p><p>It’s quite small too, measuring just 3.47 x 3.45 x 3.45 inches. The Retro Cube 2 weighs 15.87oz so it’s a little heavier than the JBL Go 5 (8.16oz), but lighter than the Tribit StormBox Mini+ (19.2oz). A wireless charging dock is included in the box so you can keep the speaker powered at all times. Both the speaker and the dock host grippy rubbers to keep them from being unintentionally knocked about.</p><h2 id="weatherproofing-5">Weatherproofing</h2><p>8BitDo doesn’t specify an IP rating for the Retro Cube 2, so it isn’t waterproof or dustproof. Given that this speaker is designed for indoor use, this isn’t a massive drawback, but something to keep in mind before you choose to take it outdoors, or use it in the shower. For a rugged waterproof speaker, I’d recommend the JBL Go 5 (IP68) or the Tribit StormBox Mini+ (IPX7).</p><h2 id="controls-15">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="r2gbEECwzShYPvvdaXy7JR" name="8bitDo_speaker_.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/r2gbEECwzShYPvvdaXy7JR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The playback controls are in the form of an NES D-pad, and are unlike any other buttons you may have seen on other speakers. The four physical buttons enable you to adjust the volume, play/pause playback, and initiate Bluetooth pairing. All buttons give great tactile feedback when pressed.</p><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4"></video></div><p>To add to the speaker’s quirkiness, when you press the power button to, well, power on the speaker, the speaker sings, “8BitDooooo.” 10/10, no notes.</p><h2 id="connectivity-11">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="n6mt4TFyBcYwpxjnHpbsrQ" name="8bitDo_speaker_ 5.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/n6mt4TFyBcYwpxjnHpbsrQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Retro Cube 2 utilizes Bluetooth 5.3 with support for the standard SBC codec. There’s no AAC or LC3, like on the JBL Go 5. You do get multipoint connectivity, though, so you can connect two devices simultaneously. I did this with my <a href="https://www.tomsguide.com/phones/google-pixel-phones/google-pixel-10-pro-xl-review">Google Pixel 10 Pro XL</a> and my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> and it worked seamlessly.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XGoAGM6nt2ZLWBYL8aAFtQ" name="8bitDo_speaker_ 6.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/XGoAGM6nt2ZLWBYL8aAFtQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In addition to Bluetooth, the Retro Cube 2 comes with a 2.4GHz dongle for virtually lag-free audio in games and music. The dongle can be stored in a slot located on the charging dock’s underside, which is a neat feature as it keeps it from getting lost. There’s wired playback too, via the USB-C port located around the back.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-sound-quality"><span>8BitDo Retro Cube 2 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4393LYVEzQ2UYQu46fH6BR" name="8bitDo_speaker_ 8.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/4393LYVEzQ2UYQu46fH6BR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>If you want a speaker to soundtrack your workday, gaming session, or an intimate dinner party, the 8BitDo Retro Cube 2 is up to the challenge. It has a power output of 10W (5W x 2) and covers a frequency response range of 125Hz to 15KHz. Given that it goes down to just 125Hz, the sub-bass and bass in general are a little lacking, but I’ll talk about that later. Good things first.</p><p>To test the Retro Cube 2, I listened to my usual selection of testing songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res, over Bluetooth. I kicked things off with ‘<a href="https://open.qobuz.com/track/80418830" target="_blank" rel="nofollow">One Inch Punch</a>’ by Yin Yin. This track highlighted the Retro Cube 2’s treble clarity, as the song’s bright surf-guitar riffs, crisp hi-hats and intricate percussion came through with plenty of energy and definition. The track sounded lively and the guitar notes cut cleanly through the mix, while percussion retained its texture without becoming harsh.</p><p>The Retro Cube 2 features a surprisingly capable midrange too. Lana Del Rey’s vocals in ‘<a href="https://open.qobuz.com/track/203578478" target="_blank" rel="nofollow">Taco Truck x VB</a>’ sat front and centre, sounding clear, intimate and well-defined which allowed the emotional nuances in her performance to come through effortlessly. The guitars blended smoothly without becoming congested, and the Retro Cube 2 lent the midrange elements a natural warmth that flattered the song’s laid-back, hazy production style.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7SRu3iTvSprF9Aa6eM77HR" name="8bitDo_speaker_ 1.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/7SRu3iTvSprF9Aa6eM77HR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>But the only disappointing bit about the Retro Cube 2 is its limited bass capabilities — something commonplace in small speakers. I played ‘<a href="https://open.qobuz.com/track/121923140" target="_blank" rel="nofollow">No Chances</a>’ by Twenty One Pilots to test the speaker’s bass extension. While the track’s ominous low-end pulse and deep synth bass were present, they lacked the weight and physical impact that lend the song much of its tension — something you won’t experience when listening to it through the JBL Go 5. The song’s cinematic foundation, then, felt noticeably reduced, and while the kickdrums had definition, they didn’t hit with the chest-thumping force the song demands.</p><p>Overall, the Retro Cube 2 performs well but doesn’t provide a sound as full as the JBL Go 5, so if you want powerful bass, you may want to consider the JBL instead. But if cute looks matter more to you, then the Retro Cube 2 is a no-brainer.</p><p><em>Listen along to my review with the 8BitDo Retro Cube 2 testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/64843841?zone=GB-en"></iframe><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-app"><span>8BitDo Retro Cube 2 review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LK6UhmxAggp5BRfE5BuGFR" name="8bitDo_speaker_ 7.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/LK6UhmxAggp5BRfE5BuGFR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The 8BitDo Retro Cube 2 doesn’t have a mobile companion app, so you can’t customize the speaker on the fly. Well, you can’t customize the speaker full stop. While the Retro Cube 2 is compatible with the <a href="https://app.8bitdo.com/Ultimate-Software-V2/" target="_blank" rel="nofollow">8BitDo Ultimate Software V2</a> macOS and Windows app, the only thing you can do is update the speaker’s firmware.</p><p>This means that there’s no custom EQ or way to remap the buttons. If you want customizable EQ, I’d recommend spending a little extra on the JBL Go 5, or getting the cheaper Tribit StormBox Mini+.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-battery-life"><span>8BitDo Retro Cube 2 review: Battery life</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="T2W7qBf7gfSdGfahHXYZ7R" name="8bitDo_speaker_ 9.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/T2W7qBf7gfSdGfahHXYZ7R.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of the 8BitDo Retro Cube 2’s biggest selling points is its long battery life, which is class-leading. 8BitDo says that the speaker can last up to 30 hours, and can go from zero to 100% in three hours. In my testing, the Retro Cube 2 ran out of battery around the 29th hour mark when it was playing at 50% volume.</p><p>The Retro Cube 2 outclasses most other speakers in its weight class. The JBL Go 5 offers a maximum playtime of 10 hours, while the Tribit StormBox Mini+ can last up to 12 hours. The Tozo PE1 has a rated battery life of 32 hours at low volume, and we found that it drained really quickly at high volumes. Given that, the Retro Cube 2 is your best option in the battery life department.</p><p>Unlike any other tiny Bluetooth speaker I’ve tested, the Retro Cube 2 comes with a wireless charging dock to keep it powered at all times.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-verdict"><span>8BitDo Retro Cube 2 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9Xq5jy2dZnQHMxMFUpEr8S" name="8bitDo_speaker_ 10.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/9Xq5jy2dZnQHMxMFUpEr8S.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Few Bluetooth speakers are as attractive as the 8BitDo Retro Cube 2. This cute little thing sports an NES-inspired design that’s sure to impress retro gamers. Its D-pad-style buttons not only give good tactile feedback when pressed, but they add to the Retro Cube 2’s charm too. Throw in 30 hours of battery life and a wireless charging dock with dongle storage and you’ve got yourself a winner.</p><p>But how does it sound? The Retro Cube 2 boasts great sound dynamics with the ability to make the treble sparkle. However, the bass is a little lacking, given that the speaker doesn’t have a wide frequency response range. It doesn’t have a custom EQ either, and it isn’t waterproof.</p><p>Should those drawbacks stop you from getting the Retro Cube 2? Depends on what you’re after. If you want a full-bodied sound, the JBL Go 5 is clearly the superior option. But if you want a $49 speaker that makes for a great addition to your setup, or as a coffee table centerpiece, the Retro Cube 2 is sure to please you. Its retro aesthetics have won me over, and it's become a permanent fixture on my desk. I’m vibing to the “8BitDoooo” startup sound.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/8bitdo-retro-cube-2-review</link>
                                                                            <description>
                            <![CDATA[ The 8BitDo Retro Cube 2 is an NES-inspired speaker with D-pad controls, and it boasts crisp treble and a refined midrange for $49. It has some flaws though. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">8ei5bCyM7HPHw2GcZGhR7K</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/eRckzAjnhpaftZk3Aif5uQ-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 02 Jun 2026 09:38:42 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:40 +0000</updated>
                                                                                                                                            <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ nikita.achanta@futurenet.com (Nikita Achanta) ]]></author>                    <dc:creator><![CDATA[ Nikita Achanta ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oXuvixDz99SbZp9z8Uoor3.png ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&#039;s Guide. She is a lifelong gaming and photography enthusiast, especially interested in wildlife photography. Having worked as a Sub Editor and Writer for Canon EMEA, she’s a bit of a grammar nerd (and a supporter of the Oxford comma), and has also interviewed photographers from all over the world and working in different genres.&lt;/p&gt;&lt;p&gt;A holder of two master’s degrees, the most recent one being in Magazine Journalism from Cardiff University, Nikita’s work has appeared in several publications such as Motor Sport Magazine, NME, Marriott Bonvoy, The Independent, and Metro. Her favorite tech includes the PS5, the DJI Air 3S, and the Fujifilm X-T50. She&#039;s also a licensed drone pilot and cameras expert so you&#039;ll find her testing those nearly every week.&lt;/p&gt;&lt;p&gt;In her downtime, Nikita can usually be found sinking hours into RPGs on her PS5, flying a drone, out on a walk with a camera in hand, at a concert, watching F1, or planning her next tattoo. You can follow her photography account on Instagram&lt;a href=&quot;https://www.instagram.com/photos.bynikita/&quot; target=&quot;_blank&quot; rel=&quot;nofollow&quot;&gt; here&lt;/a&gt;.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/eRckzAjnhpaftZk3Aif5uQ-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker]]></media:description>                                                            <media:text><![CDATA[A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker]]></media:text>
                                <media:title type="plain"><![CDATA[A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/eRckzAjnhpaftZk3Aif5uQ-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’ve tested many of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> in my time, but most of them look either the same or they sport bland or uninventive design. Not the 8BitDo Retro Cube 2. It’s the cutest, quirkiest speaker you can buy today, and not for a lot of money as it costs just $49. An NES-inspired design and D-pad-style buttons make the Retro Cube 2 a joy to look at and use.</p><p>In addition to its simply fantastic design, the Retro Cube 2 boasts long battery life with up to 30 hours of playback time. It comes with a practical wireless charging dock too. Sound quality is great, with treble that shines and strong dynamics. However, given the speaker’s size and limited frequency response, the bass is a little lacking. It isn’t waterproof either, and its underbaked companion app doesn’t do it any favors.</p><p>Should you still buy it? Read my full 8BitDo Retro Cube 2 review to find out.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-specs"><span>8BitDo Retro Cube 2 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">£39</a></p></td></tr><tr><td class="firstcol " ><p><strong>Durability</strong></p></td><td  ><p>No IP rating</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.3 with SBC, 2.4GHz, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Multi-speaker connectivity</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Output power</strong></p></td><td  ><p>5W x 2</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>120Hz-15KHz</p></td></tr><tr><td class="firstcol " ><p><strong>Range</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>30 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>3.47 x 3.45 x 3.45 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>15.87oz (speaker); 3.06oz (charging dock)</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>N Edition (beige, red and black)</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-price-availability"><span>8BitDo Retro Cube 2 review: Price & availability</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eRckzAjnhpaftZk3Aif5uQ" name="8bitDo_speaker_ 4.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/eRckzAjnhpaftZk3Aif5uQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The 8BitDo Retro Cube 2 is the successor to the first-gen speaker, which first came out in 2019. The 2026 iteration is priced similarly, and is available for <a href="https://www.amazon.com/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Portable-Bluetooth-Wireless-Charging-Connections/dp/B0G48LRFP4/" target="_blank" rel="nofollow">£39 at Amazon</a>. It’s available in one color only, at the time of writing: N Edition, which is a combination of beige, red and black.</p><p>If it’s a budget Bluetooth speaker you’re after, the Retro Cube 2 is perfect for you. It’s cheaper than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-go-5-review">JBL Go 5 </a>($54) and the Tribit Stormbox Micro 3 ($64). It’s a little pricier than the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-mini-plus-review">Tribit StormBox Mini+</a> ($39) and the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tozo-pe1-review">Tozo PE1</a> ($39) but it’s still in the same ballpark. So, why should you buy the Retro Cube 2 from a brand that isn’t as well-known for audio gear as it is for its retro-style peripherals? The design and long battery life, for starters, and we’ll get into the details next.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-design"><span>8BitDo Retro Cube 2 review: Design</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TFdpvvuvdeBc2KzxWgkD2R" name="8bitDo_speaker_ 3.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/TFdpvvuvdeBc2KzxWgkD2R.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I’ve tested a lot of Bluetooth speakers in my time, but I’ve never seen one as cute as the 8BitDo Retro Cube 2. Sporting a design inspired by the NES, the Retro Cube 2 fits right in with the brand’s other retro-style products. If you own the <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review">8BitDo Retro Mechanical Keyboard</a> like I do, or the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/8bitdo-64-review">8BitDo 64</a> N64-styled gamepad, the Retro Cube 2 is the perfect accompaniment, thanks to its beige, red and black colors.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6938px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="fjPya8gWyNVCdRvgFLQGsh" name="8BitDo-Retro-Cube-2" alt="8BitDo Retro Cube 2 and 8BitDo Retro Mechanical Keyboard" src="https://cdn.mos.cms.futurecdn.net/fjPya8gWyNVCdRvgFLQGsh.jpg" mos="" align="middle" fullscreen="" width="6938" height="3903" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Nikita Achanta / Tom's Guide)</span></figcaption></figure><p>The Retro Cube 2 is, as the name suggests, a cube. Four red speaker grilles are located on each face, and the top of the cube hosts the D-pad physical buttons, which I’ll talk about shortly. With subtle branding, the Retro Cube 2 is minimalist and good-looking, and is sure to draw eyeballs (in a good way). There’s no flashy RGB lighting, which I like, and the only bits that light up are the setting indicators on the top.</p><p>It’s quite small too, measuring just 3.47 x 3.45 x 3.45 inches. The Retro Cube 2 weighs 15.87oz so it’s a little heavier than the JBL Go 5 (8.16oz), but lighter than the Tribit StormBox Mini+ (19.2oz). A wireless charging dock is included in the box so you can keep the speaker powered at all times. Both the speaker and the dock host grippy rubbers to keep them from being unintentionally knocked about.</p><h2 id="weatherproofing-5">Weatherproofing</h2><p>8BitDo doesn’t specify an IP rating for the Retro Cube 2, so it isn’t waterproof or dustproof. Given that this speaker is designed for indoor use, this isn’t a massive drawback, but something to keep in mind before you choose to take it outdoors, or use it in the shower. For a rugged waterproof speaker, I’d recommend the JBL Go 5 (IP68) or the Tribit StormBox Mini+ (IPX7).</p><h2 id="controls-15">Controls</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="r2gbEECwzShYPvvdaXy7JR" name="8bitDo_speaker_.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/r2gbEECwzShYPvvdaXy7JR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The playback controls are in the form of an NES D-pad, and are unlike any other buttons you may have seen on other speakers. The four physical buttons enable you to adjust the volume, play/pause playback, and initiate Bluetooth pairing. All buttons give great tactile feedback when pressed.</p><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/JgfKhEpRZsTbrJGaE6QdK3/8BitDo%20%E2%80%94%20Startup%20sound.mp4"></video></div><p>To add to the speaker’s quirkiness, when you press the power button to, well, power on the speaker, the speaker sings, “8BitDooooo.” 10/10, no notes.</p><h2 id="connectivity-11">Connectivity</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="n6mt4TFyBcYwpxjnHpbsrQ" name="8bitDo_speaker_ 5.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/n6mt4TFyBcYwpxjnHpbsrQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Retro Cube 2 utilizes Bluetooth 5.3 with support for the standard SBC codec. There’s no AAC or LC3, like on the JBL Go 5. You do get multipoint connectivity, though, so you can connect two devices simultaneously. I did this with my <a href="https://www.tomsguide.com/phones/google-pixel-phones/google-pixel-10-pro-xl-review">Google Pixel 10 Pro XL</a> and my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> and it worked seamlessly.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XGoAGM6nt2ZLWBYL8aAFtQ" name="8bitDo_speaker_ 6.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/XGoAGM6nt2ZLWBYL8aAFtQ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>In addition to Bluetooth, the Retro Cube 2 comes with a 2.4GHz dongle for virtually lag-free audio in games and music. The dongle can be stored in a slot located on the charging dock’s underside, which is a neat feature as it keeps it from getting lost. There’s wired playback too, via the USB-C port located around the back.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-sound-quality"><span>8BitDo Retro Cube 2 review: Sound quality</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4393LYVEzQ2UYQu46fH6BR" name="8bitDo_speaker_ 8.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/4393LYVEzQ2UYQu46fH6BR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>If you want a speaker to soundtrack your workday, gaming session, or an intimate dinner party, the 8BitDo Retro Cube 2 is up to the challenge. It has a power output of 10W (5W x 2) and covers a frequency response range of 125Hz to 15KHz. Given that it goes down to just 125Hz, the sub-bass and bass in general are a little lacking, but I’ll talk about that later. Good things first.</p><p>To test the Retro Cube 2, I listened to my usual selection of testing songs on <a href="https://www.tomsguide.com/audio/qobuz-review">Qobuz</a>, the <a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services">best music streaming service</a> for hi-res, over Bluetooth. I kicked things off with ‘<a href="https://open.qobuz.com/track/80418830" target="_blank" rel="nofollow">One Inch Punch</a>’ by Yin Yin. This track highlighted the Retro Cube 2’s treble clarity, as the song’s bright surf-guitar riffs, crisp hi-hats and intricate percussion came through with plenty of energy and definition. The track sounded lively and the guitar notes cut cleanly through the mix, while percussion retained its texture without becoming harsh.</p><p>The Retro Cube 2 features a surprisingly capable midrange too. Lana Del Rey’s vocals in ‘<a href="https://open.qobuz.com/track/203578478" target="_blank" rel="nofollow">Taco Truck x VB</a>’ sat front and centre, sounding clear, intimate and well-defined which allowed the emotional nuances in her performance to come through effortlessly. The guitars blended smoothly without becoming congested, and the Retro Cube 2 lent the midrange elements a natural warmth that flattered the song’s laid-back, hazy production style.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7SRu3iTvSprF9Aa6eM77HR" name="8bitDo_speaker_ 1.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/7SRu3iTvSprF9Aa6eM77HR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>But the only disappointing bit about the Retro Cube 2 is its limited bass capabilities — something commonplace in small speakers. I played ‘<a href="https://open.qobuz.com/track/121923140" target="_blank" rel="nofollow">No Chances</a>’ by Twenty One Pilots to test the speaker’s bass extension. While the track’s ominous low-end pulse and deep synth bass were present, they lacked the weight and physical impact that lend the song much of its tension — something you won’t experience when listening to it through the JBL Go 5. The song’s cinematic foundation, then, felt noticeably reduced, and while the kickdrums had definition, they didn’t hit with the chest-thumping force the song demands.</p><p>Overall, the Retro Cube 2 performs well but doesn’t provide a sound as full as the JBL Go 5, so if you want powerful bass, you may want to consider the JBL instead. But if cute looks matter more to you, then the Retro Cube 2 is a no-brainer.</p><p><em>Listen along to my review with the 8BitDo Retro Cube 2 testing playlist:</em></p><iframe allow="" height="390" width="378" id="" style="" class="position-center" data-lazy-priority="low" data-lazy-src="https://widget.qobuz.com/playlist/64843841?zone=GB-en"></iframe><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-app"><span>8BitDo Retro Cube 2 review: App</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LK6UhmxAggp5BRfE5BuGFR" name="8bitDo_speaker_ 7.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/LK6UhmxAggp5BRfE5BuGFR.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The 8BitDo Retro Cube 2 doesn’t have a mobile companion app, so you can’t customize the speaker on the fly. Well, you can’t customize the speaker full stop. While the Retro Cube 2 is compatible with the <a href="https://app.8bitdo.com/Ultimate-Software-V2/" target="_blank" rel="nofollow">8BitDo Ultimate Software V2</a> macOS and Windows app, the only thing you can do is update the speaker’s firmware.</p><p>This means that there’s no custom EQ or way to remap the buttons. If you want customizable EQ, I’d recommend spending a little extra on the JBL Go 5, or getting the cheaper Tribit StormBox Mini+.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-battery-life"><span>8BitDo Retro Cube 2 review: Battery life</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="T2W7qBf7gfSdGfahHXYZ7R" name="8bitDo_speaker_ 9.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/T2W7qBf7gfSdGfahHXYZ7R.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>One of the 8BitDo Retro Cube 2’s biggest selling points is its long battery life, which is class-leading. 8BitDo says that the speaker can last up to 30 hours, and can go from zero to 100% in three hours. In my testing, the Retro Cube 2 ran out of battery around the 29th hour mark when it was playing at 50% volume.</p><p>The Retro Cube 2 outclasses most other speakers in its weight class. The JBL Go 5 offers a maximum playtime of 10 hours, while the Tribit StormBox Mini+ can last up to 12 hours. The Tozo PE1 has a rated battery life of 32 hours at low volume, and we found that it drained really quickly at high volumes. Given that, the Retro Cube 2 is your best option in the battery life department.</p><p>Unlike any other tiny Bluetooth speaker I’ve tested, the Retro Cube 2 comes with a wireless charging dock to keep it powered at all times.</p><h2 class="article-body__section" id="section-8bitdo-retro-cube-2-review-verdict"><span>8BitDo Retro Cube 2 review: Verdict</span></h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9Xq5jy2dZnQHMxMFUpEr8S" name="8bitDo_speaker_ 10.JPG" alt="A beige, red and black 8BitDo Retro Cube 2 Bluetooth speaker" src="https://cdn.mos.cms.futurecdn.net/9Xq5jy2dZnQHMxMFUpEr8S.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Few Bluetooth speakers are as attractive as the 8BitDo Retro Cube 2. This cute little thing sports an NES-inspired design that’s sure to impress retro gamers. Its D-pad-style buttons not only give good tactile feedback when pressed, but they add to the Retro Cube 2’s charm too. Throw in 30 hours of battery life and a wireless charging dock with dongle storage and you’ve got yourself a winner.</p><p>But how does it sound? The Retro Cube 2 boasts great sound dynamics with the ability to make the treble sparkle. However, the bass is a little lacking, given that the speaker doesn’t have a wide frequency response range. It doesn’t have a custom EQ either, and it isn’t waterproof.</p><p>Should those drawbacks stop you from getting the Retro Cube 2? Depends on what you’re after. If you want a full-bodied sound, the JBL Go 5 is clearly the superior option. But if you want a $49 speaker that makes for a great addition to your setup, or as a coffee table centerpiece, the Retro Cube 2 is sure to please you. Its retro aesthetics have won me over, and it's become a permanent fixture on my desk. I’m vibing to the “8BitDoooo” startup sound.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Google's new $99 smart home speaker leaks — here's how it compares to the Apple HomePod Mini and Amazon Echo Dot Max ]]></title>
                                                                                                <dc:content><![CDATA[ <p>In October 2025, Google <a href="https://www.tomsguide.com/home/smart-home/googles-next-gemini-powered-home-speaker-actually-appeared-during-pixel-10-launch-what-we-know">teased its next Google Home speaker</a>, but declined to provide any release or pricing details. However, thanks to new listings on Best Buy we may have both. </p><p>The <a href="https://www.tomsguide.com/us/google-home-smart-speaker,review-4010.html">original Google Home speaker</a> released in 2020, which we described as great if you're in the Google system, but it lacked key features compared to competitors. The new version makes an aesthetic leap that appears more similar to the Apple HomePod Mini and the Echo Dot.</p><p>We found a <a href="https://www.bestbuy.com/product/google-home-speaker-coming-spring-2026-smart-speaker-with-gemini-for-home-voice-assistant-and-360-degree-sound-hazel/J39TC8JG7G" target="_blank">Best Buy listing </a>for the Google Home speaker that reveals a price of $99. It also appears to come in four colors: berry, hazel, jade and porcelain. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-exmBPW"></div>                            </div>                            <script src="https://kwizly.com/embed/exmBPW.js" async></script><p>The new speaker is "engineered for Gemini," which includes Gemini Live. This version would be able to control your smart home, set up automations and play media. Apparently, it will also be able to detect sounds like alarms going off and breaking glass — responding by sending alerts to your phone.</p><p>Earlier today, the <a href="https://www.bestbuy.ca/en-ca/product/google-home-audio-smart-speaker-with-google-assistant-hazel-coming-spring-2026/19399724" target="_blank">Canadian version of Best Buy</a> listed a release date of June 25, 2026. It appears that was a mistake since that information has been scrubbed from the listing as of this writing. Both listings now say "coming Spring 2026." </p><h2 id="how-it-compares-to-the-homepod-mini-and-echo-dot">How it compares to the HomePod mini and Echo Dot</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="qzn6VBsNWsuQVgzKVVPvhA" name="Apple HomePod mini midnight.jpg" alt="Apple HomePod mini in midnight color" src="https://cdn.mos.cms.futurecdn.net/qzn6VBsNWsuQVgzKVVPvhA.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Apple)</span></figcaption></figure><p>Apple is expected to <a href="https://www.tomsguide.com/home/smart-home/apple-could-launch-7-new-smart-home-devices-this-year-but-theres-a-catch">launch new Gemini/Siri-powered versions</a> of its HomePod devices later this year at which point this comparison will have to change.  </p><p>Right now, the HomePod mini costs $99. By comparison, the newest model of the Echo Dot costs $49. Though unlike the HomePod and Google's speaker, it comes in a variety of versions including kids and sports-themed ones that have slightly higher prices but still not $100. The larger<a href="https://www.tomsguide.com/audio/smart-speakers/amazon-echo-dot-max-review"> Echo Dot Max </a>is $99 and works great with Alexa+ but isn't really that much of an upgrade over the Dot.</p><p>We took an i<a href="https://www.tomsguide.com/audio/smart-speakers/i-need-new-smart-speakers-for-my-home-heres-how-im-deciding-between-amazon-google-apple-and-sonos">n-depth look at the Echo versus the HomePod mini</a> and the current Google Nest Audio in October last year. Check out for more concrete details.</p><p>Simply, the Echo combined with Alexa Plus works great with Amazon devices like the Ring Doorbell. The audio is a downfall but good enough. </p><p>The HomePod mini, meanwhile, has easy setup isn't as compatible with smart devices. The juice is in the sound, which is excellent. The issue is that Apple doesn't have as robust a smart home ecosystem like Google and Amazon, though that is something Apple reportedly wants to change.</p><p>Once Google does release its new Home speaker, we'll give it a full test to see how it stacks up. Hopefully, we'll actually have it in our hands at the end of June.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/phones/samsung-galaxy-z-fold-8-vs-google-pixel-11-pro-fold-which-android-foldable-will-win-in-2026">Samsung Galaxy Z Fold 8 vs Google Pixel 11 Pro Fold: Which Android foldable will win in 2026?</a></li><li><a href="https://www.tomsguide.com/tvs/want-to-master-your-smart-tv-here-are-5-google-tv-features-you-probably-dont-use-but-definitely-should">5 Google TV features you're probably not using but definitely should</a></li><li><a href="https://www.tomsguide.com/computing/internet/i-hate-that-i-love-googles-new-ai-powered-search-bar-it-makes-the-internet-easier-to-digest-but-my-career-is-cooked-now">I hate that I love Google’s new AI-powered search bar — it makes the internet easier to digest, but my career is cooked now</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/googles-new-usd99-smart-home-speaker-leaks-heres-how-it-compares-to-the-apple-homepod-mini-and-amazon-echo-dot-max</link>
                                                                            <description>
                            <![CDATA[ A surprise leak on Best Buy appears to reveal the next Google Home speaker launching in late June. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">pWhCdYKVfatzNXLg8jX25R</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/zaz4nFR4yutzwBNjFLnaPj-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 02 Jun 2026 00:12:14 +0000</pubDate>                                                                                                                                <updated>Tue, 02 Jun 2026 00:12:20 +0000</updated>
                                                                                                                                            <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ scott.younker@futurenet.com (Scott Younker) ]]></author>                    <dc:creator><![CDATA[ Scott Younker ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/RZsUpqcJ6Uj2q83oCUwNhQ.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Scott Younker is the West Coast Reporter at Tom’s Guide. He covers all the latest tech news, including phones, computing and more. He’s been involved in tech since 2011 covering everything from cameras and swimming pool equipment to the latest gaming consoles and smart TVs. He is on a seemingly never ending hunt to build the easiest to use home media system. &lt;/p&gt;&lt;p&gt;Before Tom’s Guide, Scott worked for publications like &lt;em&gt;Digital Trends, Outdoor Photographer, Dead Beats Panel&lt;/em&gt;, and in a brief detour, &lt;em&gt;America’s Funniest Home Videos&lt;/em&gt;. Yes, he has seen more pratfalls, silly pets and ridiculous home movie fails than is reasonably healthy. &lt;/p&gt;&lt;p&gt;When not writing about the latest devices or advances in chipsets, be sure to ask about Scott about disc golf and sustainability, or just if you’re being cheeky, ask about his noodle arm. If you truly want to get nerdy, bring up board games and his ongoing losing streak. &lt;/p&gt;&lt;p&gt;Scott joined Tom&#039;s Guide in 2024 as the West Coast Reporter. He graduated from the School of Journalism at the University of Oregon with a degree in magazine journalism and a minor in communications. While there he blogged or wrote for several magazines including the Fluxx, Ethos and the Oregon Commentator. He briefly wrote and managed a moderately successful blog focused on web comics. &lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/zaz4nFR4yutzwBNjFLnaPj-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Google Home Speaker]]></media:description>                                                            <media:text><![CDATA[Google Home Speaker]]></media:text>
                                <media:title type="plain"><![CDATA[Google Home Speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/zaz4nFR4yutzwBNjFLnaPj-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>In October 2025, Google <a href="https://www.tomsguide.com/home/smart-home/googles-next-gemini-powered-home-speaker-actually-appeared-during-pixel-10-launch-what-we-know">teased its next Google Home speaker</a>, but declined to provide any release or pricing details. However, thanks to new listings on Best Buy we may have both. </p><p>The <a href="https://www.tomsguide.com/us/google-home-smart-speaker,review-4010.html">original Google Home speaker</a> released in 2020, which we described as great if you're in the Google system, but it lacked key features compared to competitors. The new version makes an aesthetic leap that appears more similar to the Apple HomePod Mini and the Echo Dot.</p><p>We found a <a href="https://www.bestbuy.com/product/google-home-speaker-coming-spring-2026-smart-speaker-with-gemini-for-home-voice-assistant-and-360-degree-sound-hazel/J39TC8JG7G" target="_blank">Best Buy listing </a>for the Google Home speaker that reveals a price of $99. It also appears to come in four colors: berry, hazel, jade and porcelain. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-exmBPW"></div>                            </div>                            <script src="https://kwizly.com/embed/exmBPW.js" async></script><p>The new speaker is "engineered for Gemini," which includes Gemini Live. This version would be able to control your smart home, set up automations and play media. Apparently, it will also be able to detect sounds like alarms going off and breaking glass — responding by sending alerts to your phone.</p><p>Earlier today, the <a href="https://www.bestbuy.ca/en-ca/product/google-home-audio-smart-speaker-with-google-assistant-hazel-coming-spring-2026/19399724" target="_blank">Canadian version of Best Buy</a> listed a release date of June 25, 2026. It appears that was a mistake since that information has been scrubbed from the listing as of this writing. Both listings now say "coming Spring 2026." </p><h2 id="how-it-compares-to-the-homepod-mini-and-echo-dot">How it compares to the HomePod mini and Echo Dot</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="qzn6VBsNWsuQVgzKVVPvhA" name="Apple HomePod mini midnight.jpg" alt="Apple HomePod mini in midnight color" src="https://cdn.mos.cms.futurecdn.net/qzn6VBsNWsuQVgzKVVPvhA.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Apple)</span></figcaption></figure><p>Apple is expected to <a href="https://www.tomsguide.com/home/smart-home/apple-could-launch-7-new-smart-home-devices-this-year-but-theres-a-catch">launch new Gemini/Siri-powered versions</a> of its HomePod devices later this year at which point this comparison will have to change.  </p><p>Right now, the HomePod mini costs $99. By comparison, the newest model of the Echo Dot costs $49. Though unlike the HomePod and Google's speaker, it comes in a variety of versions including kids and sports-themed ones that have slightly higher prices but still not $100. The larger<a href="https://www.tomsguide.com/audio/smart-speakers/amazon-echo-dot-max-review"> Echo Dot Max </a>is $99 and works great with Alexa+ but isn't really that much of an upgrade over the Dot.</p><p>We took an i<a href="https://www.tomsguide.com/audio/smart-speakers/i-need-new-smart-speakers-for-my-home-heres-how-im-deciding-between-amazon-google-apple-and-sonos">n-depth look at the Echo versus the HomePod mini</a> and the current Google Nest Audio in October last year. Check out for more concrete details.</p><p>Simply, the Echo combined with Alexa Plus works great with Amazon devices like the Ring Doorbell. The audio is a downfall but good enough. </p><p>The HomePod mini, meanwhile, has easy setup isn't as compatible with smart devices. The juice is in the sound, which is excellent. The issue is that Apple doesn't have as robust a smart home ecosystem like Google and Amazon, though that is something Apple reportedly wants to change.</p><p>Once Google does release its new Home speaker, we'll give it a full test to see how it stacks up. Hopefully, we'll actually have it in our hands at the end of June.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/phones/samsung-galaxy-z-fold-8-vs-google-pixel-11-pro-fold-which-android-foldable-will-win-in-2026">Samsung Galaxy Z Fold 8 vs Google Pixel 11 Pro Fold: Which Android foldable will win in 2026?</a></li><li><a href="https://www.tomsguide.com/tvs/want-to-master-your-smart-tv-here-are-5-google-tv-features-you-probably-dont-use-but-definitely-should">5 Google TV features you're probably not using but definitely should</a></li><li><a href="https://www.tomsguide.com/computing/internet/i-hate-that-i-love-googles-new-ai-powered-search-bar-it-makes-the-internet-easier-to-digest-but-my-career-is-cooked-now">I hate that I love Google’s new AI-powered search bar — it makes the internet easier to digest, but my career is cooked now</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ This affordable, pocket-sized Bluetooth speaker is on sale for less than $25 — here's 11 more speaker deals to get the party going ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Summer is here, and that means outdoor parties, picnics, hikes, camping and road trips. Everything in summer is way more fun with your favorite tunes pumping, so I've been searching for the best Bluetooth speaker deals to get you amazing sound quality without breaking the bank.</p><p>Right now you can get the <a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow">Tribit PocketGo for $24 at Amazon</a>. It's the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> on a budget — my colleagues were shocked at how amazing its sound quality is, and it truly outperforms its small size and small price tag. Make sure to snag one while you can.</p><p>There are lots more Bluetooth speaker deals out there from top audio brands, so keep scrolling to see my favorites. I also recommend checking out our <a href="https://www.tomsguide.com/coupons/amazon.com">Amazon promo codes</a>.</p><h2 class="article-body__section" id="section-quick-links"><span>Quick Links</span></h2><ul><li><a href="https://www.amazon.com/b?_encoding=UTF8&node=7073956011" target="_blank" rel="nofollow"><strong>shop Bluetooth speaker deals at Amazon</strong></a></li><li><strong>Tribit PocketGo: </strong><a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><strong>was $29 now $24</strong></a></li><li><strong>Ultimate Ears WonderBoom 4: </strong><a href="https://www.amazon.com/dp/B0BRXJVG1S" target="_blank" rel="nofollow"><strong>was $99 now $51</strong></a></li><li><strong>Soundcore Boom 2: </strong><a href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><strong>was $139 now $89</strong></a></li><li><strong>JBL Flip 6: </strong><a href="https://www.amazon.com/JBL-Flip-Portable-Waterproof-Speaker/dp/B09RQ47HC8/ref" target="_blank" rel="nofollow"><strong>was $129 now $89</strong></a></li><li><strong>Bose SoundLink Micro Portable (2nd Gen): </strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow"><strong>was $129 now $99</strong></a></li><li><strong>Marshall Stanmore III: </strong><a href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59" target="_blank" rel="nofollow"><strong>was $399 now $299</strong></a><strong> </strong></li></ul><h2 class="article-body__section" id="section-best-bluetooth-speaker-deals"><span>Best Bluetooth speaker deals</span></h2><div class="product"><a data-dimension112="57ccf1d7-9b1f-4693-a430-f797ab937986" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$24" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="XnqELzjemVWDKqXxoESeBh" name="tribit-pocketgo-bluetooth-shower-speaker-e7119067-af9e-4c09-9feb-6f1f593c89d6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XnqELzjemVWDKqXxoESeBh.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review" data-dimension112="57ccf1d7-9b1f-4693-a430-f797ab937986" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$24">Tribit PocketGo</a> will be your new best friend. For less than $30, this Bluetooth speaker packs one heck of a punch in the sound department. On top of that, it’s the ultimate rugged speaker. Its IP68 waterproofing and military-grade drop protection make it perfect for long hikes in unpredictable weather. And I do mean <em>long</em> hikes because the speaker has a total battery life of 20 hours, making it ideal for a weekend camping trip. <a class="view-deal button" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow" data-dimension112="57ccf1d7-9b1f-4693-a430-f797ab937986" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$24">View Deal</a></p></div><div class="product"><a data-dimension112="63fb91fa-8472-4a1a-878a-7988816e6615" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1425px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="D7QYGUpe98kCsZkVddg4oV" name="Ultimate Ears Wonderboom 4.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/D7QYGUpe98kCsZkVddg4oV.jpg" mos="" align="middle" fullscreen="" width="1425" height="1425" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The mighty and portable Wonderboom 4 is a great outdoor speaker that goes louder than it has any right to. In our <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ue-wonderboom-4-review" data-dimension112="63fb91fa-8472-4a1a-878a-7988816e6615" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">Ultimate Ears WonderBoom 4 review</a>, our reviewer loved its compact size, very good 16-hour battery life and slick rugged looks. It's IP67 water and dust-resistant as well, so you can really take it anywhere to listen to your tunes.<a class="view-deal button" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow" data-dimension112="63fb91fa-8472-4a1a-878a-7988816e6615" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">View Deal</a></p></div><div class="product"><a data-dimension112="3e744887-8eb6-40e9-ad93-f3354cea8f2d" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$59" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1110px;"><p class="vanilla-image-block" style="padding-top:118.65%;"><img id="UYAz4ytBrnKLyZwUopY4fS" name="StormBox Micro 3" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/UYAz4ytBrnKLyZwUopY4fS.jpg" mos="" align="middle" fullscreen="" width="1110" height="1317" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="3e744887-8eb6-40e9-ad93-f3354cea8f2d" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$59">StormBox Micro 3: was $64 now $59</a></strong><br>We rank the StormBox Micro 2 as the best Bluetooth speaker for travel, and this new iteration looks like more of a good thing. I just got my hands on this one myself, and I'm already loving the magnetic design and carry strap — it's extremely easy to take along or set up on a magnetic surface.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="3e744887-8eb6-40e9-ad93-f3354cea8f2d" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$59">View Deal</a></p></div><div class="product"><a data-dimension112="a4801986-08e5-4cb3-b7c9-996fa5055660" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$89" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="3CvgYRTFMbxneUZcepKkae" name="soundcore boom 2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/3CvgYRTFMbxneUZcepKkae.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A speaker of this caliber for just $89 is unheard of. The Boom 2 has a massive 80W power, meaning it will go <em>loud</em>. Like the much pricier JBL options, the Boom 2 has a 24 hour battery life and IP67 rating, so it'll be able to follow you anywhere. <a class="view-deal button" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow" data-dimension112="a4801986-08e5-4cb3-b7c9-996fa5055660" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="485932c0-c3ef-4154-86ee-5eeeefaa49e9" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="CUupDdwp3iFPpTNoLMAwEg" name="marshall-willen-ii-portable-bluetooth-sp-2bd48523-b998-4c85-b830-a731963cc40d.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/CUupDdwp3iFPpTNoLMAwEg.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Willen II is a tiny powerhouse of a speaker that packs quality sound and good battery life into a compact package. It'll last for 17 hours, which is longer than some of the larger competitors. This little speaker is well worth the pickup.<a class="view-deal button" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow" data-dimension112="485932c0-c3ef-4154-86ee-5eeeefaa49e9" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="cf0750f0-bdd8-47b5-959d-4238241380b0" data-action="Deal Block" data-label="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension48="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension25="$99" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1105px;"><p class="vanilla-image-block" style="padding-top:110.05%;"><img id="W9bhTKNwnzv3HgRUn6A2sc" name="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/W9bhTKNwnzv3HgRUn6A2sc.jpg" mos="" align="middle" fullscreen="" width="1105" height="1216" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow" data-dimension112="cf0750f0-bdd8-47b5-959d-4238241380b0" data-action="Deal Block" data-label="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension48="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension25="$99">SoundLink Micro Portable Bluetooth Speaker (2nd Gen): was $129 now $99</a></strong><br>Powerful Bose sound in a tiny package! This speaker packs a punch with crisp, bassy sound, IP67 water and dust resistance and 12 hours of battery life. The color options are stylish, too.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow" data-dimension112="cf0750f0-bdd8-47b5-959d-4238241380b0" data-action="Deal Block" data-label="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension48="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension25="$99">View Deal</a></p></div><div class="product"><a data-dimension112="313957f5-fbf2-4fcc-9aa0-1464b9ab102d" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$89" href="https://www.amazon.com/JBL-Flip-Portable-Waterproof-Speaker/dp/B09RQ47HC8" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="33AJemCSzK8fNYEhkUfBtm" name="JBL Flip 6" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/33AJemCSzK8fNYEhkUfBtm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>JBL just doesn't miss with its Bluetooth speakers. Although the Flip 7 came out earlier this year, the Flip 6 shouldn't be forgotten about. It's now on sale and an awesome deal for this pocket-sized 30W speaker. It's IP67 rated and has a 12 hour battery life, meaning it's perfect to chuck in your backpack for all day outdoor listening. <a class="view-deal button" href="https://www.amazon.com/JBL-Flip-Portable-Waterproof-Speaker/dp/B09RQ47HC8" target="_blank" rel="nofollow" data-dimension112="313957f5-fbf2-4fcc-9aa0-1464b9ab102d" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="93f486a2-a5f2-431c-902c-e99df9039999" data-action="Deal Block" data-label="Charge 6" data-dimension48="Charge 6" data-dimension25="$159" href="https://www.amazon.com/JBL-Charge-Waterproof-Drop-Proof-Built/dp/B0G7D9BLYJ" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:60.93%;"><img id="fAV3y9WMvrAFWQKezHyaAh" name="JBL Charge 6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/fAV3y9WMvrAFWQKezHyaAh.jpg" mos="" align="middle" fullscreen="" width="1500" height="914" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The JBL Charge 6 is a great speaker with plenty of battery life and some great sound quality. 28 hours of staying power means the tunes will keep on coming. The color options are wicked too, letting you really show off (or blend in) as much as you want.<a class="view-deal button" href="https://www.amazon.com/JBL-Charge-Waterproof-Drop-Proof-Built/dp/B0G7D9BLYJ" target="_blank" rel="nofollow" data-dimension112="93f486a2-a5f2-431c-902c-e99df9039999" data-action="Deal Block" data-label="Charge 6" data-dimension48="Charge 6" data-dimension25="$159">View Deal</a></p></div><div class="product"><a data-dimension112="6f6766cf-33fa-4aff-bf2d-1e773748a447" data-action="Deal Block" data-label="Marshall Acton III" data-dimension48="Marshall Acton III" data-dimension25="$199" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Cream/dp/B0BBYF2SXX" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="VK72LAYkKKEKEWGPQxGoBn" name="marshall-acton-iii-bluetooth-home-speake-9c29b8fe-0398-481f-b43e-eb4a003dadde.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/VK72LAYkKKEKEWGPQxGoBn.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Acton III is a retro-looking Bluetooth speaker that's can actually make your setup feel more modern. The physical controls and tactile and satisfying, making sure it's easy for you to get the sound you want. It needs to be plugged into the wall to work, but that doesn't stop it from producing some of the best sound on the market.<a class="view-deal button" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Cream/dp/B0BBYF2SXX" target="_blank" rel="nofollow" data-dimension112="6f6766cf-33fa-4aff-bf2d-1e773748a447" data-action="Deal Block" data-label="Marshall Acton III" data-dimension48="Marshall Acton III" data-dimension25="$199">View Deal</a></p></div><div class="product"><a data-dimension112="4cdca56d-f77f-4b47-b824-65ab24c474f3" data-action="Deal Block" data-label="Boombox 3" data-dimension48="Boombox 3" data-dimension25="$299" href="https://www.amazon.com/JBL-Boombox-Waterproof-PartyBoost-eco-Friendly/dp/B09YDS8BHK" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1562px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="mz3Lg9DhD3d25zqFxXtyCS" name="JBL Boombox 3 (1).jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/mz3Lg9DhD3d25zqFxXtyCS.jpg" mos="" align="middle" fullscreen="" width="1562" height="879" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>JBL's awesome Boombox range is like the Xtreme line on steroids. In our 4-star <a href="https://www.tomsguide.com/reviews/jbl-boombox-3" data-dimension112="4cdca56d-f77f-4b47-b824-65ab24c474f3" data-action="Deal Block" data-label="Boombox 3" data-dimension48="Boombox 3" data-dimension25="$299">JBL Boombox 3 review</a>, we loved the booming bass and the Boombox 3's ability to fill a large outdoor area. Although it's a heavy speaker, the rugged design and all-day (24 hour) battery life means it'll keep the party going regardless. It'll play even in torrential rain — it's IP67 rated.<a class="view-deal button" href="https://www.amazon.com/JBL-Boombox-Waterproof-PartyBoost-eco-Friendly/dp/B09YDS8BHK" target="_blank" rel="nofollow" data-dimension112="4cdca56d-f77f-4b47-b824-65ab24c474f3" data-action="Deal Block" data-label="Boombox 3" data-dimension48="Boombox 3" data-dimension25="$299">View Deal</a></p></div><div class="product"><a data-dimension112="127125be-37ad-40b3-a26e-cd8cc5eaa6e2" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$299" href="https://www.amazon.com/dp/B09HTFCR59" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="wYnAg4Aq77FqRs4Sxa3xn7" name="marshall stanmore iii" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wYnAg4Aq77FqRs4Sxa3xn7.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Marshall consistently makes some of the best audio gear out there, and the Stanmore III is no different. This awesome amp-lookalike speaker has a 3.5mm jack input and RCA input, so you can hook it up for even higher-quality music. The speaker has designated bass and treble dials so you can turn up the bass all the way if you so desire. <a class="view-deal button" href="https://www.amazon.com/dp/B09HTFCR59" target="_blank" rel="nofollow" data-dimension112="127125be-37ad-40b3-a26e-cd8cc5eaa6e2" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$299">View Deal</a></p></div><div class="product"><a data-dimension112="388f10b2-6d1a-4d57-835a-9edcb7a15cc5" data-action="Deal Block" data-label="Kilburn III" data-dimension48="Kilburn III" data-dimension25="$349" href="https://www.amazon.com/dp/B0F967JT5Y" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1509px;"><p class="vanilla-image-block" style="padding-top:99.34%;"><img id="h3j5SdPPY2co2Kf2LhGvk3" name="Marshall Kilburn III.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/h3j5SdPPY2co2Kf2LhGvk3.jpg" mos="" align="middle" fullscreen="" width="1509" height="1499" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The epic-looking <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review" data-dimension112="388f10b2-6d1a-4d57-835a-9edcb7a15cc5" data-action="Deal Block" data-label="Kilburn III" data-dimension48="Kilburn III" data-dimension25="$349">Marshall Kilburn III</a> is more than just a pretty face — it also sounds amazing too. The battery life is extremely impressive, and the on-device tone controls make keeping the music going easier than anything else. It also, you know, looks really cool. We currently rank it as the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> you can buy.<a class="view-deal button" href="https://www.amazon.com/dp/B0F967JT5Y" target="_blank" rel="nofollow" data-dimension112="388f10b2-6d1a-4d57-835a-9edcb7a15cc5" data-action="Deal Block" data-label="Kilburn III" data-dimension48="Kilburn III" data-dimension25="$349">View Deal</a></p></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/this-affordable-pocket-sized-bluetooth-speaker-is-on-sale-for-less-than-usd25-heres-11-more-speaker-deals-to-get-the-party-going</link>
                                                                            <description>
                            <![CDATA[ Some of the best speakers we've tested are on sale this summer — check out these deals to get the biggest sound quality at tiny prices. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">kmrrfuyWk3BY9aUcQoJCw4</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/w76fCuYEJvmP6PEU5C2BrH-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 29 May 2026 11:25:39 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ millie.daviswilliams@futurenet.com (Millie Davis-Williams) ]]></author>                    <dc:creator><![CDATA[ Millie Davis-Williams ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/WU6pa7hKuxs5ScCoqcXYWA.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Millie is a Deals Editor at Tom&#039;s Guide specializing in tech deals content. Based in London, Millie has spent a lifetime stubbornly refusing to pay full price on any possible purchase. She has years of searching for the best discounts on the latest and greatest phones, TVs and gadgets under her belt, and knows how to sort the best deals from the rest. Helping readers save on the products they want is her mission, whether it’s an Apple Watch or a new tablet.&lt;/p&gt;&lt;p&gt;Since working at Tom’s Guide, Millie has covered retail events like Prime Day, Black Friday and Cyber Monday extensively, meaning she knows the products readers are looking for and the best times of year to hit “purchase.”&lt;/p&gt;&lt;p&gt;When she’s not looking for discounts, Millie covers news and writes how-to articles about everything from phones, streaming devices, and headphones to apps and video games.&lt;/p&gt;&lt;p&gt;In her spare time, Millie enjoys reading, gaming on her Nintendo Switch and creating digital art. She’s also a fan of watching Disney movies and collecting plush (the latter having totally taken over her bedroom.)&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/w76fCuYEJvmP6PEU5C2BrH-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Tribit PocketGo Bluetooth Speaker]]></media:description>                                                            <media:text><![CDATA[Tribit PocketGo Bluetooth Speaker]]></media:text>
                                <media:title type="plain"><![CDATA[Tribit PocketGo Bluetooth Speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/w76fCuYEJvmP6PEU5C2BrH-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Summer is here, and that means outdoor parties, picnics, hikes, camping and road trips. Everything in summer is way more fun with your favorite tunes pumping, so I've been searching for the best Bluetooth speaker deals to get you amazing sound quality without breaking the bank.</p><p>Right now you can get the <a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow">Tribit PocketGo for $24 at Amazon</a>. It's the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> on a budget — my colleagues were shocked at how amazing its sound quality is, and it truly outperforms its small size and small price tag. Make sure to snag one while you can.</p><p>There are lots more Bluetooth speaker deals out there from top audio brands, so keep scrolling to see my favorites. I also recommend checking out our <a href="https://www.tomsguide.com/coupons/amazon.com">Amazon promo codes</a>.</p><h2 class="article-body__section" id="section-quick-links"><span>Quick Links</span></h2><ul><li><a href="https://www.amazon.com/b?_encoding=UTF8&node=7073956011" target="_blank" rel="nofollow"><strong>shop Bluetooth speaker deals at Amazon</strong></a></li><li><strong>Tribit PocketGo: </strong><a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><strong>was $29 now $24</strong></a></li><li><strong>Ultimate Ears WonderBoom 4: </strong><a href="https://www.amazon.com/dp/B0BRXJVG1S" target="_blank" rel="nofollow"><strong>was $99 now $51</strong></a></li><li><strong>Soundcore Boom 2: </strong><a href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><strong>was $139 now $89</strong></a></li><li><strong>JBL Flip 6: </strong><a href="https://www.amazon.com/JBL-Flip-Portable-Waterproof-Speaker/dp/B09RQ47HC8/ref" target="_blank" rel="nofollow"><strong>was $129 now $89</strong></a></li><li><strong>Bose SoundLink Micro Portable (2nd Gen): </strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow"><strong>was $129 now $99</strong></a></li><li><strong>Marshall Stanmore III: </strong><a href="https://www.amazon.com/Marshall-Stanmore-Bluetooth-Wireless-Speaker/dp/B09HTFCR59" target="_blank" rel="nofollow"><strong>was $399 now $299</strong></a><strong> </strong></li></ul><h2 class="article-body__section" id="section-best-bluetooth-speaker-deals"><span>Best Bluetooth speaker deals</span></h2><div class="product"><a data-dimension112="57ccf1d7-9b1f-4693-a430-f797ab937986" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$24" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="XnqELzjemVWDKqXxoESeBh" name="tribit-pocketgo-bluetooth-shower-speaker-e7119067-af9e-4c09-9feb-6f1f593c89d6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XnqELzjemVWDKqXxoESeBh.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review" data-dimension112="57ccf1d7-9b1f-4693-a430-f797ab937986" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$24">Tribit PocketGo</a> will be your new best friend. For less than $30, this Bluetooth speaker packs one heck of a punch in the sound department. On top of that, it’s the ultimate rugged speaker. Its IP68 waterproofing and military-grade drop protection make it perfect for long hikes in unpredictable weather. And I do mean <em>long</em> hikes because the speaker has a total battery life of 20 hours, making it ideal for a weekend camping trip. <a class="view-deal button" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow" data-dimension112="57ccf1d7-9b1f-4693-a430-f797ab937986" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$24">View Deal</a></p></div><div class="product"><a data-dimension112="63fb91fa-8472-4a1a-878a-7988816e6615" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1425px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="D7QYGUpe98kCsZkVddg4oV" name="Ultimate Ears Wonderboom 4.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/D7QYGUpe98kCsZkVddg4oV.jpg" mos="" align="middle" fullscreen="" width="1425" height="1425" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The mighty and portable Wonderboom 4 is a great outdoor speaker that goes louder than it has any right to. In our <a href="https://www.tomsguide.com/audio/bluetooth-speakers/ue-wonderboom-4-review" data-dimension112="63fb91fa-8472-4a1a-878a-7988816e6615" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">Ultimate Ears WonderBoom 4 review</a>, our reviewer loved its compact size, very good 16-hour battery life and slick rugged looks. It's IP67 water and dust-resistant as well, so you can really take it anywhere to listen to your tunes.<a class="view-deal button" href="https://www.amazon.com/dp/B0BRXJVG1S/" target="_blank" rel="nofollow" data-dimension112="63fb91fa-8472-4a1a-878a-7988816e6615" data-action="Deal Block" data-label="WonderBoom 4" data-dimension48="WonderBoom 4" data-dimension25="$51">View Deal</a></p></div><div class="product"><a data-dimension112="3e744887-8eb6-40e9-ad93-f3354cea8f2d" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$59" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1110px;"><p class="vanilla-image-block" style="padding-top:118.65%;"><img id="UYAz4ytBrnKLyZwUopY4fS" name="StormBox Micro 3" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/UYAz4ytBrnKLyZwUopY4fS.jpg" mos="" align="middle" fullscreen="" width="1110" height="1317" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="3e744887-8eb6-40e9-ad93-f3354cea8f2d" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$59">StormBox Micro 3: was $64 now $59</a></strong><br>We rank the StormBox Micro 2 as the best Bluetooth speaker for travel, and this new iteration looks like more of a good thing. I just got my hands on this one myself, and I'm already loving the magnetic design and carry strap — it's extremely easy to take along or set up on a magnetic surface.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0GF6PPS4S" target="_blank" rel="nofollow" data-dimension112="3e744887-8eb6-40e9-ad93-f3354cea8f2d" data-action="Deal Block" data-label="StormBox Micro 3" data-dimension48="StormBox Micro 3" data-dimension25="$59">View Deal</a></p></div><div class="product"><a data-dimension112="a4801986-08e5-4cb3-b7c9-996fa5055660" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$89" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="3CvgYRTFMbxneUZcepKkae" name="soundcore boom 2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/3CvgYRTFMbxneUZcepKkae.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>A speaker of this caliber for just $89 is unheard of. The Boom 2 has a massive 80W power, meaning it will go <em>loud</em>. Like the much pricier JBL options, the Boom 2 has a 24 hour battery life and IP67 rating, so it'll be able to follow you anywhere. <a class="view-deal button" href="https://www.amazon.com/Soundcore-Subwoofer-Waterproof-Floatable-Bluetooth/dp/B0CQ53RVTW" target="_blank" rel="nofollow" data-dimension112="a4801986-08e5-4cb3-b7c9-996fa5055660" data-action="Deal Block" data-label="Boom 2" data-dimension48="Boom 2" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="485932c0-c3ef-4154-86ee-5eeeefaa49e9" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="CUupDdwp3iFPpTNoLMAwEg" name="marshall-willen-ii-portable-bluetooth-sp-2bd48523-b998-4c85-b830-a731963cc40d.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/CUupDdwp3iFPpTNoLMAwEg.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Willen II is a tiny powerhouse of a speaker that packs quality sound and good battery life into a compact package. It'll last for 17 hours, which is longer than some of the larger competitors. This little speaker is well worth the pickup.<a class="view-deal button" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow" data-dimension112="485932c0-c3ef-4154-86ee-5eeeefaa49e9" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="cf0750f0-bdd8-47b5-959d-4238241380b0" data-action="Deal Block" data-label="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension48="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension25="$99" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1105px;"><p class="vanilla-image-block" style="padding-top:110.05%;"><img id="W9bhTKNwnzv3HgRUn6A2sc" name="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/W9bhTKNwnzv3HgRUn6A2sc.jpg" mos="" align="middle" fullscreen="" width="1105" height="1216" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow" data-dimension112="cf0750f0-bdd8-47b5-959d-4238241380b0" data-action="Deal Block" data-label="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension48="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension25="$99">SoundLink Micro Portable Bluetooth Speaker (2nd Gen): was $129 now $99</a></strong><br>Powerful Bose sound in a tiny package! This speaker packs a punch with crisp, bassy sound, IP67 water and dust resistance and 12 hours of battery life. The color options are stylish, too.<a class="view-deal button" href="https://www.amazon.com/Bose-SoundLink-Bluetooth-Waterproof-Dustproof/dp/B0FDLD34V5" target="_blank" rel="nofollow" data-dimension112="cf0750f0-bdd8-47b5-959d-4238241380b0" data-action="Deal Block" data-label="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension48="SoundLink Micro Portable Bluetooth Speaker (2nd Gen)" data-dimension25="$99">View Deal</a></p></div><div class="product"><a data-dimension112="313957f5-fbf2-4fcc-9aa0-1464b9ab102d" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$89" href="https://www.amazon.com/JBL-Flip-Portable-Waterproof-Speaker/dp/B09RQ47HC8" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:120.00%;"><img id="33AJemCSzK8fNYEhkUfBtm" name="JBL Flip 6" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/33AJemCSzK8fNYEhkUfBtm.jpg" mos="" align="middle" fullscreen="" width="1000" height="1200" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>JBL just doesn't miss with its Bluetooth speakers. Although the Flip 7 came out earlier this year, the Flip 6 shouldn't be forgotten about. It's now on sale and an awesome deal for this pocket-sized 30W speaker. It's IP67 rated and has a 12 hour battery life, meaning it's perfect to chuck in your backpack for all day outdoor listening. <a class="view-deal button" href="https://www.amazon.com/JBL-Flip-Portable-Waterproof-Speaker/dp/B09RQ47HC8" target="_blank" rel="nofollow" data-dimension112="313957f5-fbf2-4fcc-9aa0-1464b9ab102d" data-action="Deal Block" data-label="Flip 6" data-dimension48="Flip 6" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="93f486a2-a5f2-431c-902c-e99df9039999" data-action="Deal Block" data-label="Charge 6" data-dimension48="Charge 6" data-dimension25="$159" href="https://www.amazon.com/JBL-Charge-Waterproof-Drop-Proof-Built/dp/B0G7D9BLYJ" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:60.93%;"><img id="fAV3y9WMvrAFWQKezHyaAh" name="JBL Charge 6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/fAV3y9WMvrAFWQKezHyaAh.jpg" mos="" align="middle" fullscreen="" width="1500" height="914" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The JBL Charge 6 is a great speaker with plenty of battery life and some great sound quality. 28 hours of staying power means the tunes will keep on coming. The color options are wicked too, letting you really show off (or blend in) as much as you want.<a class="view-deal button" href="https://www.amazon.com/JBL-Charge-Waterproof-Drop-Proof-Built/dp/B0G7D9BLYJ" target="_blank" rel="nofollow" data-dimension112="93f486a2-a5f2-431c-902c-e99df9039999" data-action="Deal Block" data-label="Charge 6" data-dimension48="Charge 6" data-dimension25="$159">View Deal</a></p></div><div class="product"><a data-dimension112="6f6766cf-33fa-4aff-bf2d-1e773748a447" data-action="Deal Block" data-label="Marshall Acton III" data-dimension48="Marshall Acton III" data-dimension25="$199" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Cream/dp/B0BBYF2SXX" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="VK72LAYkKKEKEWGPQxGoBn" name="marshall-acton-iii-bluetooth-home-speake-9c29b8fe-0398-481f-b43e-eb4a003dadde.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/VK72LAYkKKEKEWGPQxGoBn.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Marshall Acton III is a retro-looking Bluetooth speaker that's can actually make your setup feel more modern. The physical controls and tactile and satisfying, making sure it's easy for you to get the sound you want. It needs to be plugged into the wall to work, but that doesn't stop it from producing some of the best sound on the market.<a class="view-deal button" href="https://www.amazon.com/Marshall-Acton-Bluetooth-Speaker-Cream/dp/B0BBYF2SXX" target="_blank" rel="nofollow" data-dimension112="6f6766cf-33fa-4aff-bf2d-1e773748a447" data-action="Deal Block" data-label="Marshall Acton III" data-dimension48="Marshall Acton III" data-dimension25="$199">View Deal</a></p></div><div class="product"><a data-dimension112="4cdca56d-f77f-4b47-b824-65ab24c474f3" data-action="Deal Block" data-label="Boombox 3" data-dimension48="Boombox 3" data-dimension25="$299" href="https://www.amazon.com/JBL-Boombox-Waterproof-PartyBoost-eco-Friendly/dp/B09YDS8BHK" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1562px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="mz3Lg9DhD3d25zqFxXtyCS" name="JBL Boombox 3 (1).jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/mz3Lg9DhD3d25zqFxXtyCS.jpg" mos="" align="middle" fullscreen="" width="1562" height="879" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>JBL's awesome Boombox range is like the Xtreme line on steroids. In our 4-star <a href="https://www.tomsguide.com/reviews/jbl-boombox-3" data-dimension112="4cdca56d-f77f-4b47-b824-65ab24c474f3" data-action="Deal Block" data-label="Boombox 3" data-dimension48="Boombox 3" data-dimension25="$299">JBL Boombox 3 review</a>, we loved the booming bass and the Boombox 3's ability to fill a large outdoor area. Although it's a heavy speaker, the rugged design and all-day (24 hour) battery life means it'll keep the party going regardless. It'll play even in torrential rain — it's IP67 rated.<a class="view-deal button" href="https://www.amazon.com/JBL-Boombox-Waterproof-PartyBoost-eco-Friendly/dp/B09YDS8BHK" target="_blank" rel="nofollow" data-dimension112="4cdca56d-f77f-4b47-b824-65ab24c474f3" data-action="Deal Block" data-label="Boombox 3" data-dimension48="Boombox 3" data-dimension25="$299">View Deal</a></p></div><div class="product"><a data-dimension112="127125be-37ad-40b3-a26e-cd8cc5eaa6e2" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$299" href="https://www.amazon.com/dp/B09HTFCR59" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1000px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="wYnAg4Aq77FqRs4Sxa3xn7" name="marshall stanmore iii" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wYnAg4Aq77FqRs4Sxa3xn7.jpg" mos="" align="middle" fullscreen="" width="1000" height="1000" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Marshall consistently makes some of the best audio gear out there, and the Stanmore III is no different. This awesome amp-lookalike speaker has a 3.5mm jack input and RCA input, so you can hook it up for even higher-quality music. The speaker has designated bass and treble dials so you can turn up the bass all the way if you so desire. <a class="view-deal button" href="https://www.amazon.com/dp/B09HTFCR59" target="_blank" rel="nofollow" data-dimension112="127125be-37ad-40b3-a26e-cd8cc5eaa6e2" data-action="Deal Block" data-label="Stanmore III" data-dimension48="Stanmore III" data-dimension25="$299">View Deal</a></p></div><div class="product"><a data-dimension112="388f10b2-6d1a-4d57-835a-9edcb7a15cc5" data-action="Deal Block" data-label="Kilburn III" data-dimension48="Kilburn III" data-dimension25="$349" href="https://www.amazon.com/dp/B0F967JT5Y" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1509px;"><p class="vanilla-image-block" style="padding-top:99.34%;"><img id="h3j5SdPPY2co2Kf2LhGvk3" name="Marshall Kilburn III.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/h3j5SdPPY2co2Kf2LhGvk3.jpg" mos="" align="middle" fullscreen="" width="1509" height="1499" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The epic-looking <a href="https://www.tomsguide.com/audio/bluetooth-speakers/marshall-kilburn-iii-review" data-dimension112="388f10b2-6d1a-4d57-835a-9edcb7a15cc5" data-action="Deal Block" data-label="Kilburn III" data-dimension48="Kilburn III" data-dimension25="$349">Marshall Kilburn III</a> is more than just a pretty face — it also sounds amazing too. The battery life is extremely impressive, and the on-device tone controls make keeping the music going easier than anything else. It also, you know, looks really cool. We currently rank it as the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> you can buy.<a class="view-deal button" href="https://www.amazon.com/dp/B0F967JT5Y" target="_blank" rel="nofollow" data-dimension112="388f10b2-6d1a-4d57-835a-9edcb7a15cc5" data-action="Deal Block" data-label="Kilburn III" data-dimension48="Kilburn III" data-dimension25="$349">View Deal</a></p></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tested LG’s first FlexConnect soundbar for 3 months — this could be the future of surround sound ]]></title>
                                                                                                <dc:content><![CDATA[ <div  class="fancy-box"><div class="fancy_box-title">LG Sound Suite H7: Specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price: </strong>$999 / £899<br><strong>Connectivity: </strong>HDMI eARC, Bluetooth, Wi-Fi (AirPlay 2, Google Cast)<br><strong>Weight: </strong>16.9 lbs<br><strong>Dimensions: </strong>47.2 x 2.5 x 5.6 inches<br><strong>Colors: </strong>Black<br><strong>Subwoofer: </strong>No<br><strong>Dolby Atmos: </strong>Yes<br><strong>Channel: </strong>9.1.6</p></div></div><p>LG Sound Suite — a new blend of LG audio hardware and Dolby FlexConnect software — <a href="https://www.tomsguide.com/audio/speakers/the-lg-sound-suite-soundbar-changed-the-way-i-think-about-surround-sound"><u>blew me away at CES</u></a> when I saw it in early January. The technology, though still in its infancy, is going to change the way we think about surround sound as it allows you to place speakers anywhere in your room and get calibrated spatial audio sound tuned to wherever you want to sit. </p><p>Part and parcel of the technology is the LG Sound Suite H7 Soundbar. Technically, it’s possible to run the Sound Suite software through an <a href="https://www.tomsguide.com/tvs/oled-tvs/lg-c5-oled-tv-review"><u>LG C5</u></a>, <a href="https://www.tomsguide.com/tvs/oled-tvs/lg-g5-oled-tv-review"><u>G5 OLED</u></a> or one of LG’s new 2026 TVs, but this soundbar enables any TV with an eARC port to partake in FlexConnect’s … well, flexibility. </p><p>As the brains behind the brawn of LG’s new platform, the H7 has a lot to offer. Unfortunately, this <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbar</a> competitor itself isn’t quite as versatile as the technology driving it. Here’s why. </p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-cheat-sheet"><span>LG Sound Suite H7 review: Cheat sheet</span></h3><ul><li><strong>What is it? </strong>A Dolby Atmos soundbar that can connect wirelessly to up to four FlexConnect speakers and a subwoofer</li><li><strong>Who is it for? </strong>Anyone looking for an alternative to Sonos or Samsung’s all-in-one audio boxes</li><li><strong>What does it cost?</strong> <a href="https://www.lg.com/us/soundbars/lg-sound-suite-h7-soundbar"><u>$999</u></a> / <a href="https://www.lg.com/uk/soundsuite/h7/?srsltid=AfmBOorU_fx8zXsUIsyidmTV4NDcIsR1J4rdjK71aJWWf7ATpNoNDb7S"><u>£899</u></a></li><li><strong>What do we like? </strong>The FlexConnect integration enables you to set up surround speakers anywhere in your room</li><li><strong>What don’t we like? </strong>The aforementioned surround speakers don’t come in the box and will set you back an additional $249 apiece</li></ul><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-price-and-release-date"><span>LG Sound Suite H7 review: Price and release date</span></h3><p>The LG Sound Suite H7 is one of those rare products that gets announced at CES in early January and then becomes available to buy shortly after the show ends; it went on sale in mid-January 2026 at a launch price of $999 at <a href="https://www.bestbuy.com/product/lg-sound-suite-h7-soundbar-with-dolby-atmos-flexconnect-black/JJ8VPZKTPY"><u>Best Buy</u></a> and <a href="https://www.amazon.com/LG-Sound-Suite-H7-Soundbar/dp/B0G76G6NCP/"><u>Amazon</u></a>. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YLDYx5ttQzcqc6aiYvz8si" name="LG Sound Suite H7 Soundbar-1" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/YLDYx5ttQzcqc6aiYvz8si.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Of course, if you only buy the soundbar, you’re not really getting Sound Suite’s full functionality — you’ll at least need a few of the Sound Suite M5 or M7 Speakers to go with it. Those will set you back an additional $249 or $399, respectively. Finally, if you want to add a subwoofer to the mix, you’re looking at an additional $599 for the Sound Suite W7 sub. </p><p>If you went all-in on a full Sound Suite system, you’re looking at around $3,200 — much more than it would cost for a comparable Dolby Atmos system if you build it piecemeal. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZLDjHw8H2aocSnHwQqXAri" name="LG Sound Suite H7 Soundbar-6" alt="Top view of the LG Sound Suite M7 Wireless Speaker" src="https://cdn.mos.cms.futurecdn.net/ZLDjHw8H2aocSnHwQqXAri.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This is similar, in some ways, to how a Sonos system works. You’d start by buying the Sonos Arc Ultra for close to $1,000, then you’d tack on a <a href="https://www.tomsguide.com/reviews/sonos-sub-mini"><u>Sonos Sub Mini</u></a> ($399), a pair of <a href="https://www.tomsguide.com/reviews/sonos-era-300"><u>Sonos Era 300</u></a> ($379 apiece) or both. You’ll get a more robust sound that way, but you can see where LG got its Sound Suite pricing model from. </p><p>Your other option for a similar setup would be the Samsung HW-Q990F, an 11.1.4 system-in-a-box that includes a subwoofer and two satellite speakers for $1,800. This is the cheaper option, but will offer less flexibility in speaker placement than LG’s new Sound Suite system. </p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-design-and-controls"><span>LG Sound Suite H7 review: Design and controls</span></h3><p>LG designed the Sound Suite H7 as a 9.1.6-channel soundbar; it’s impressive that LG managed to cram 12 speakers and eight passive radiators into the soundbar’s 47.2 x 2.5 x 5.6 inch chassis. There’s not enough room between those speakers to truly feel like distinct channels — an issue I’ll cover more when we talk about its performance — but it certainly packs a punch.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZGqAURgygrBzjeRpzjv6zi" name="LG Sound Suite H7 Soundbar-5" alt="Control buttons on the LG Sound Suite H7 Soundbar" src="https://cdn.mos.cms.futurecdn.net/ZGqAURgygrBzjeRpzjv6zi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Spin it around to the backside and you’ll see the port…er, ports, <em>technically</em> because there are two of them. Only two of them. There’s the HDMI-In port that you’ll connect via a cable to the TV and a USB 3.0 port that can be used to host local media. </p><p>There are no other ports here. No optical audio (likely omitted because Dolby Atmos requires an eARC connection) or HDMI passthrough ports. The latter is unfortunate, as it means you’re going to lose a port on your TV without any form of recompense and it’s something we see often on rival soundbars.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="65duhzVjSJh2LwmUuqH3pi" name="LG Sound Suite H7 Soundbar-8" alt="LG Sound Suite M7 Wireless Speaker on a stand" src="https://cdn.mos.cms.futurecdn.net/65duhzVjSJh2LwmUuqH3pi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>For better and worse, every connection besides the TV is wireless. The satellite speakers and subwoofer connect cable-free, and music playback comes courtesy of AirPlay 2 and Google Chromecast. There's even Bluetooth for more old-school wireless connections. Unfortunately, wireless connectivity on board the H7 isn't always the most stable, which creates issues.</p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-features"><span>LG Sound Suite H7 review: Features</span></h3><p>The reason you’d buy an LG Sound Suite speaker instead of, say, something from Sonos or Samsung, is for its Dolby FlexConnect features. The ability to place speakers anywhere in the room and have them play perfectly attuned spatial audio never fails to impress me. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sUNHNXt9zZtuBB6bk6Mmyi" name="LG Sound Suite H7 Soundbar-2" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/sUNHNXt9zZtuBB6bk6Mmyi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Of course, even better than simply having a room that’s set up for spatial audio is having a central “sweet spot” where you get the best sound possible. LG has enabled this through a technology it’s calling Sound Follow, and it’s like having a calibrator on-call 24/7. Sitting in the sweet spot had audible advantages to sitting on the periphery of the system, and became a spot everyone in the house would race towards when we sat down to watch a movie.</p><p>When it comes to wireless playback, the H7 supports Bluetooth connectivity alongside Apple AirPlay2 and Google Cast. If you want to get really fancy, there’s also Tidal Connect and Spotify Connect supported right out of the box as well. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sCHmyscQp6FMpPxhoS7Jmi" name="LG Sound Suite H7 Soundbar-9" alt="Remote for the LG Sound Suite H7 Soundbar" src="https://cdn.mos.cms.futurecdn.net/sCHmyscQp6FMpPxhoS7Jmi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Of course, LG’s ThinQ app is the bridge between you and the soundbar. This is the place you’ll go to get the speakers connected to your Wi-Fi network and, eventually, paired together. There’s not a lot of functionality in the app, but it’s here you’ll find the all-important Sound Follow feature. If you ever need to change the Wi-Fi settings, you’ll come back to the app to do that, but otherwise I didn’t find myself using it all that often.</p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-sound-quality"><span>LG Sound Suite H7 review: Sound quality</span></h3><p>As a standalone soundbar, I wasn’t wildly impressed with the H7 — it’s loud but not well-rounded. But pairing it up with the M7 speakers was a totally different experience.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4vtC5NuDhukxRpipYXN9pi" name="LG Sound Suite H7 Soundbar-7" alt="Top view of the LG Sound Suite M7 Wireless Speaker" src="https://cdn.mos.cms.futurecdn.net/4vtC5NuDhukxRpipYXN9pi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>My biggest issue with multi-channel performance — and this applied whether I was watching a movie or listening to music — is that the speakers would sometimes drop the connection. Each speaker requires its own internet connection, and that can lead to just one of them dropping out. They’d usually re-connect within a few seconds, but it wasn’t the best user experience.</p><p><strong>Movies</strong></p><p>To see how it fared with movies, I loaded up the Dolby Atmos-enabled “Inside Out 2” on Disney Plus. This film is not only a visual treat — and absolutely heartwarming — but it’s got a great audio track that’s perfect for speaker testing. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="a6b3HfNCYCjXQY6F2TZKxi" name="LG Sound Suite H7 Soundbar-3" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/a6b3HfNCYCjXQY6F2TZKxi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I noticed right away that, on its own, the H7 couldn’t convey convincing Dolby Atmos. That’s not uncommon on unibody soundbars, but it was slightly more painful given the price. Dialogue was mixed, with some scenes having extremely easy to hear conversations, while other scenes required me to turn up the volume to hear what was being said. </p><p>Without a subwoofer, I didn’t get a lot of low-end extension, but again, this isn’t uncommon for a unibody soundbar. Still, musical swells in the movie’s main theme and certain action sequences (pretty much anytime there was hockey involved) all sounded great.</p><p><strong>Music</strong></p><p>As with movie soundtracks, bass frequencies can get lost while listening to standard two-channel albums — but thankfully, the soundstage is fairly wide thanks to those 12 speakers and eight passive radiators. Placing instruments in their exact locations was a struggle, but live albums like David Gilmour’s “Live at Pompeii” felt truly immersive. This is the first time I’ve heard a live album on a soundbar system and walked away impressed.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HNZ6E8GhkjgsmhNEc8UL2j" name="LG Sound Suite H7 Soundbar-4" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/HNZ6E8GhkjgsmhNEc8UL2j.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Studio albums like The Format’s new “Boycott Heaven” also leaned into the soundbar’s strengths — Nate Ruess’ impressive vocal range sounded excellent across the board.</p><p>Overall, the H7’s performance is a lot like Ruess’ former band — exciting and dynamic, but just missing that special something that we’ve seen in the Sonos Arc Ultra.</p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-verdict"><span>LG Sound Suite H7 review: Verdict</span></h3><p>The LG Sound Suite H7 is the start of something big. I’m still genuinely amazed by Dolby Atmos FlexConnect and LG’s implementation of the technology, despite the connectivity issues I faced. This is the first time I’ve ever wanted to use a soundbar for a live album instead of my stereo speakers. That’s a massive achievement. </p><p>Problematically, however, the soundbar is a bit rich. At a lower price or in a total package that includes all the speakers and a subwoofer for under $1,800, the H7 and its Sound Suite stablemates would be an easier recommendation. But the fact that you can get a whole surround system in the form of the Samsung HW-Q990F for a fraction of the cost makes it a lukewarm deal in a hotbed of competition.</p><p>Once it drops down in price, I’ll be more forgiving of its shortcomings and I’m very much looking forward to seeing FlexConnect blossom in the next few years.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/lg-sound-suite-h7-soundbar-review</link>
                                                                            <description>
                            <![CDATA[ The LG Sound Suite H7 is the start of something big, but because it lacks a wireless subwoofer or rear speakers inside the box, it’s a tougher sell. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">Ma9prvTQ8WEHpAsCRegj7B</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/aVVuJR7A2WQQsDYSN3RD9V-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 28 May 2026 09:30:00 +0000</pubDate>                                                                                                                                <updated>Thu, 28 May 2026 18:48:27 +0000</updated>
                                                                                                                                            <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ Nick.Pino@futurenet.com (Nick Pino) ]]></author>                    <dc:creator><![CDATA[ Nick Pino ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/7xnnEdyK5eEbDVbS5pYB54.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nick Pino heads up the TV and AV verticals at Tom&#039;s Guide and covers everything from OLED TVs to the latest wireless headphones. He&#039;s a Level 1 Certified THX Home Theater Professional, and has attended classes on TV and audio calibration. On Tom&#039;s Guide, he looks after some of the site&#039;s important buying guides like the best TVs, best OLED TVs, best 4K TVs, so most of his day is spent watching and evaluating new screens from LG, Samsung, Sony, Hisense, TCL and Vizio. (And yes, he knows how lucky he is to say that.) He was formerly the Senior Editor, TV and AV at TechRadar (Tom&#039;s Guide&#039;s sister site) and has previously written for GamesRadar, Official Xbox Magazine, PC Gamer and other outlets over the last decade. He got his start on Best Buy&#039;s official video game magazine, @GAMER, for whom he attended his first E3 in 2013. He&#039;s served as a judge for CES&#039; prestigious Innovation Awards and has attended the tech conference in Las Vegas since 2014. He has a computer science degree from the University at Buffalo and still resides in the Buffalo area. (Go Bills!) Not sure which TV you should buy? Drop him an email or tweet him on Twitter and he can help you out.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/aVVuJR7A2WQQsDYSN3RD9V-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[LG Sound Suite H7 Soundbar]]></media:description>                                                            <media:text><![CDATA[LG Sound Suite H7 Soundbar]]></media:text>
                                <media:title type="plain"><![CDATA[LG Sound Suite H7 Soundbar]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/aVVuJR7A2WQQsDYSN3RD9V-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <div  class="fancy-box"><div class="fancy_box-title">LG Sound Suite H7: Specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price: </strong>$999 / £899<br><strong>Connectivity: </strong>HDMI eARC, Bluetooth, Wi-Fi (AirPlay 2, Google Cast)<br><strong>Weight: </strong>16.9 lbs<br><strong>Dimensions: </strong>47.2 x 2.5 x 5.6 inches<br><strong>Colors: </strong>Black<br><strong>Subwoofer: </strong>No<br><strong>Dolby Atmos: </strong>Yes<br><strong>Channel: </strong>9.1.6</p></div></div><p>LG Sound Suite — a new blend of LG audio hardware and Dolby FlexConnect software — <a href="https://www.tomsguide.com/audio/speakers/the-lg-sound-suite-soundbar-changed-the-way-i-think-about-surround-sound"><u>blew me away at CES</u></a> when I saw it in early January. The technology, though still in its infancy, is going to change the way we think about surround sound as it allows you to place speakers anywhere in your room and get calibrated spatial audio sound tuned to wherever you want to sit. </p><p>Part and parcel of the technology is the LG Sound Suite H7 Soundbar. Technically, it’s possible to run the Sound Suite software through an <a href="https://www.tomsguide.com/tvs/oled-tvs/lg-c5-oled-tv-review"><u>LG C5</u></a>, <a href="https://www.tomsguide.com/tvs/oled-tvs/lg-g5-oled-tv-review"><u>G5 OLED</u></a> or one of LG’s new 2026 TVs, but this soundbar enables any TV with an eARC port to partake in FlexConnect’s … well, flexibility. </p><p>As the brains behind the brawn of LG’s new platform, the H7 has a lot to offer. Unfortunately, this <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbar</a> competitor itself isn’t quite as versatile as the technology driving it. Here’s why. </p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-cheat-sheet"><span>LG Sound Suite H7 review: Cheat sheet</span></h3><ul><li><strong>What is it? </strong>A Dolby Atmos soundbar that can connect wirelessly to up to four FlexConnect speakers and a subwoofer</li><li><strong>Who is it for? </strong>Anyone looking for an alternative to Sonos or Samsung’s all-in-one audio boxes</li><li><strong>What does it cost?</strong> <a href="https://www.lg.com/us/soundbars/lg-sound-suite-h7-soundbar"><u>$999</u></a> / <a href="https://www.lg.com/uk/soundsuite/h7/?srsltid=AfmBOorU_fx8zXsUIsyidmTV4NDcIsR1J4rdjK71aJWWf7ATpNoNDb7S"><u>£899</u></a></li><li><strong>What do we like? </strong>The FlexConnect integration enables you to set up surround speakers anywhere in your room</li><li><strong>What don’t we like? </strong>The aforementioned surround speakers don’t come in the box and will set you back an additional $249 apiece</li></ul><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-price-and-release-date"><span>LG Sound Suite H7 review: Price and release date</span></h3><p>The LG Sound Suite H7 is one of those rare products that gets announced at CES in early January and then becomes available to buy shortly after the show ends; it went on sale in mid-January 2026 at a launch price of $999 at <a href="https://www.bestbuy.com/product/lg-sound-suite-h7-soundbar-with-dolby-atmos-flexconnect-black/JJ8VPZKTPY"><u>Best Buy</u></a> and <a href="https://www.amazon.com/LG-Sound-Suite-H7-Soundbar/dp/B0G76G6NCP/"><u>Amazon</u></a>. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YLDYx5ttQzcqc6aiYvz8si" name="LG Sound Suite H7 Soundbar-1" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/YLDYx5ttQzcqc6aiYvz8si.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Of course, if you only buy the soundbar, you’re not really getting Sound Suite’s full functionality — you’ll at least need a few of the Sound Suite M5 or M7 Speakers to go with it. Those will set you back an additional $249 or $399, respectively. Finally, if you want to add a subwoofer to the mix, you’re looking at an additional $599 for the Sound Suite W7 sub. </p><p>If you went all-in on a full Sound Suite system, you’re looking at around $3,200 — much more than it would cost for a comparable Dolby Atmos system if you build it piecemeal. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZLDjHw8H2aocSnHwQqXAri" name="LG Sound Suite H7 Soundbar-6" alt="Top view of the LG Sound Suite M7 Wireless Speaker" src="https://cdn.mos.cms.futurecdn.net/ZLDjHw8H2aocSnHwQqXAri.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>This is similar, in some ways, to how a Sonos system works. You’d start by buying the Sonos Arc Ultra for close to $1,000, then you’d tack on a <a href="https://www.tomsguide.com/reviews/sonos-sub-mini"><u>Sonos Sub Mini</u></a> ($399), a pair of <a href="https://www.tomsguide.com/reviews/sonos-era-300"><u>Sonos Era 300</u></a> ($379 apiece) or both. You’ll get a more robust sound that way, but you can see where LG got its Sound Suite pricing model from. </p><p>Your other option for a similar setup would be the Samsung HW-Q990F, an 11.1.4 system-in-a-box that includes a subwoofer and two satellite speakers for $1,800. This is the cheaper option, but will offer less flexibility in speaker placement than LG’s new Sound Suite system. </p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-design-and-controls"><span>LG Sound Suite H7 review: Design and controls</span></h3><p>LG designed the Sound Suite H7 as a 9.1.6-channel soundbar; it’s impressive that LG managed to cram 12 speakers and eight passive radiators into the soundbar’s 47.2 x 2.5 x 5.6 inch chassis. There’s not enough room between those speakers to truly feel like distinct channels — an issue I’ll cover more when we talk about its performance — but it certainly packs a punch.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZGqAURgygrBzjeRpzjv6zi" name="LG Sound Suite H7 Soundbar-5" alt="Control buttons on the LG Sound Suite H7 Soundbar" src="https://cdn.mos.cms.futurecdn.net/ZGqAURgygrBzjeRpzjv6zi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Spin it around to the backside and you’ll see the port…er, ports, <em>technically</em> because there are two of them. Only two of them. There’s the HDMI-In port that you’ll connect via a cable to the TV and a USB 3.0 port that can be used to host local media. </p><p>There are no other ports here. No optical audio (likely omitted because Dolby Atmos requires an eARC connection) or HDMI passthrough ports. The latter is unfortunate, as it means you’re going to lose a port on your TV without any form of recompense and it’s something we see often on rival soundbars.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="65duhzVjSJh2LwmUuqH3pi" name="LG Sound Suite H7 Soundbar-8" alt="LG Sound Suite M7 Wireless Speaker on a stand" src="https://cdn.mos.cms.futurecdn.net/65duhzVjSJh2LwmUuqH3pi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>For better and worse, every connection besides the TV is wireless. The satellite speakers and subwoofer connect cable-free, and music playback comes courtesy of AirPlay 2 and Google Chromecast. There's even Bluetooth for more old-school wireless connections. Unfortunately, wireless connectivity on board the H7 isn't always the most stable, which creates issues.</p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-features"><span>LG Sound Suite H7 review: Features</span></h3><p>The reason you’d buy an LG Sound Suite speaker instead of, say, something from Sonos or Samsung, is for its Dolby FlexConnect features. The ability to place speakers anywhere in the room and have them play perfectly attuned spatial audio never fails to impress me. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sUNHNXt9zZtuBB6bk6Mmyi" name="LG Sound Suite H7 Soundbar-2" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/sUNHNXt9zZtuBB6bk6Mmyi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Of course, even better than simply having a room that’s set up for spatial audio is having a central “sweet spot” where you get the best sound possible. LG has enabled this through a technology it’s calling Sound Follow, and it’s like having a calibrator on-call 24/7. Sitting in the sweet spot had audible advantages to sitting on the periphery of the system, and became a spot everyone in the house would race towards when we sat down to watch a movie.</p><p>When it comes to wireless playback, the H7 supports Bluetooth connectivity alongside Apple AirPlay2 and Google Cast. If you want to get really fancy, there’s also Tidal Connect and Spotify Connect supported right out of the box as well. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sCHmyscQp6FMpPxhoS7Jmi" name="LG Sound Suite H7 Soundbar-9" alt="Remote for the LG Sound Suite H7 Soundbar" src="https://cdn.mos.cms.futurecdn.net/sCHmyscQp6FMpPxhoS7Jmi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Of course, LG’s ThinQ app is the bridge between you and the soundbar. This is the place you’ll go to get the speakers connected to your Wi-Fi network and, eventually, paired together. There’s not a lot of functionality in the app, but it’s here you’ll find the all-important Sound Follow feature. If you ever need to change the Wi-Fi settings, you’ll come back to the app to do that, but otherwise I didn’t find myself using it all that often.</p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-sound-quality"><span>LG Sound Suite H7 review: Sound quality</span></h3><p>As a standalone soundbar, I wasn’t wildly impressed with the H7 — it’s loud but not well-rounded. But pairing it up with the M7 speakers was a totally different experience.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4vtC5NuDhukxRpipYXN9pi" name="LG Sound Suite H7 Soundbar-7" alt="Top view of the LG Sound Suite M7 Wireless Speaker" src="https://cdn.mos.cms.futurecdn.net/4vtC5NuDhukxRpipYXN9pi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>My biggest issue with multi-channel performance — and this applied whether I was watching a movie or listening to music — is that the speakers would sometimes drop the connection. Each speaker requires its own internet connection, and that can lead to just one of them dropping out. They’d usually re-connect within a few seconds, but it wasn’t the best user experience.</p><p><strong>Movies</strong></p><p>To see how it fared with movies, I loaded up the Dolby Atmos-enabled “Inside Out 2” on Disney Plus. This film is not only a visual treat — and absolutely heartwarming — but it’s got a great audio track that’s perfect for speaker testing. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="a6b3HfNCYCjXQY6F2TZKxi" name="LG Sound Suite H7 Soundbar-3" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/a6b3HfNCYCjXQY6F2TZKxi.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>I noticed right away that, on its own, the H7 couldn’t convey convincing Dolby Atmos. That’s not uncommon on unibody soundbars, but it was slightly more painful given the price. Dialogue was mixed, with some scenes having extremely easy to hear conversations, while other scenes required me to turn up the volume to hear what was being said. </p><p>Without a subwoofer, I didn’t get a lot of low-end extension, but again, this isn’t uncommon for a unibody soundbar. Still, musical swells in the movie’s main theme and certain action sequences (pretty much anytime there was hockey involved) all sounded great.</p><p><strong>Music</strong></p><p>As with movie soundtracks, bass frequencies can get lost while listening to standard two-channel albums — but thankfully, the soundstage is fairly wide thanks to those 12 speakers and eight passive radiators. Placing instruments in their exact locations was a struggle, but live albums like David Gilmour’s “Live at Pompeii” felt truly immersive. This is the first time I’ve heard a live album on a soundbar system and walked away impressed.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HNZ6E8GhkjgsmhNEc8UL2j" name="LG Sound Suite H7 Soundbar-4" alt="LG Sound Suite H7 Soundbar on a TV stand" src="https://cdn.mos.cms.futurecdn.net/HNZ6E8GhkjgsmhNEc8UL2j.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Studio albums like The Format’s new “Boycott Heaven” also leaned into the soundbar’s strengths — Nate Ruess’ impressive vocal range sounded excellent across the board.</p><p>Overall, the H7’s performance is a lot like Ruess’ former band — exciting and dynamic, but just missing that special something that we’ve seen in the Sonos Arc Ultra.</p><h3 class="article-body__section" id="section-lg-sound-suite-h7-review-verdict"><span>LG Sound Suite H7 review: Verdict</span></h3><p>The LG Sound Suite H7 is the start of something big. I’m still genuinely amazed by Dolby Atmos FlexConnect and LG’s implementation of the technology, despite the connectivity issues I faced. This is the first time I’ve ever wanted to use a soundbar for a live album instead of my stereo speakers. That’s a massive achievement. </p><p>Problematically, however, the soundbar is a bit rich. At a lower price or in a total package that includes all the speakers and a subwoofer for under $1,800, the H7 and its Sound Suite stablemates would be an easier recommendation. But the fact that you can get a whole surround system in the form of the Samsung HW-Q990F for a fraction of the cost makes it a lukewarm deal in a hotbed of competition.</p><p>Once it drops down in price, I’ll be more forgiving of its shortcomings and I’m very much looking forward to seeing FlexConnect blossom in the next few years.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I test Bluetooth speakers for a living, and these are the 3 I'll be using all summer long — and they start at just $20 on sale ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I test audio gear for a living, and that means I get my hands on some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> on the market. But in the hours I spend testing these speakers, there are only a few that I keep going back to. </p><p>And these top choice speakers are the proof you need that you don't need to spend big bucks to get awesome sound because they're all on sale. In fact, the priciest one is just $111. </p><p>All of these speakers have impressive battery life, durability protection for the outdoors, and most importantly, impressive sound. So without further ado, these are the speakers I'll be reaching for all summer long. </p><ul><li><strong>Tribit PocketGo: </strong><a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><strong>was $29 now $19 at Amazon</strong></a></li><li><strong>Marshall Willen II: </strong><a href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow"><strong>was $129 now $89 at Amazon</strong></a><strong></strong></li><li><strong>Tribit Stormbox Lava: </strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2" target="_blank" rel="nofollow"><strong>was $139 now $111 at Amazon</strong></a></li></ul><h2 id="tribit-pocketgo">Tribit PocketGo</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BzZgxLUemS3J7E73zpJd3a" name="The Tribit PocketGo is the speaker that I will now be bringing with me everywhere, from picnics to hikes. It has awesome sound and can get super loud despite being compact and lightweight. It offers 20 hours of battery life, even with the incredibly loud volumes.  My only qualm with the speaker is that it requires some EQ alterations in the app, depending on the genre you’re listening to. But when it can offer a waterproof rating of IP68 and military-grade drop protection at such a low price, I can’t deny that this is the ultimate adventure speaker." alt="The Tribit PocketGo speaker in black photographed on a blue background." src="https://cdn.mos.cms.futurecdn.net/BzZgxLUemS3J7E73zpJd3a.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a> is a speaker I use almost daily, and it comes absolutely everywhere with me. In fact, it's coming on vacation with me next week so I can listen to my favorite tracks by the pool. And I don't even have to worry about it getting wet because it has an impressive durability rating of IP68. </p><p>This sub-$30 speaker is nothing short of impressive. It sounds like it should be triple its size when it measures just 4.3 x 3.2 x 1.7 inches. But the 20 hours of battery life are what really win me over. Even at the highest volumes, the battery lasts me days at a time, although this will depend on your own listening habits. </p><div class="product"><a data-dimension112="ca21c58f-b788-4737-a1f4-f1a955d9a872" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$19" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="yFUH5DvQg4Bso6HJiAXHdM" name="tribit-pocketgo-bluetooth-shower-speaker-d7445412-d4ef-4b07-979b-7089fe237b0e.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/yFUH5DvQg4Bso6HJiAXHdM.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Tribit PocketGo is a rugged speaker with an IP68 waterproof rating, making it ideal for adventures, along with military-grade drop protection. After making some minor EQ adjustments in the Tribit app, the speaker sounds great and reaches super loud volumes.<a class="view-deal button" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow" data-dimension112="ca21c58f-b788-4737-a1f4-f1a955d9a872" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$19">View Deal</a></p></div><h2 id="marshall-willen-ii">Marshall Willen II</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dgijJ7akaPXRoD3qRDA2Go" name="Marshall Willen II" alt="Marshall Willen II" src="https://cdn.mos.cms.futurecdn.net/dgijJ7akaPXRoD3qRDA2Go.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>This is the speaker I get out when I want to show off a little. The <a href="https://www.tomsguide.com/audio/marshall-willen-ii-review-rugged-rockin-fun">Marshall Willen II</a> is the sleek answer to the world of portable Bluetooth speakers, and it's not hard to see why. Just look at it. </p><p>It's compact, measuring 3.9 x 3.9 x 1.7 inches, and is as light as a feather, weighing just 0.79 lbs. It's has 17 hours of battery to soundtrack your whole day outdoors without needing to worry about a charger, and the durability rating of IP67 means that the speaker can withstand rain or shine. </p><div class="product"><a data-dimension112="ebde716e-cea6-4f2b-bf87-0ca2c308112c" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="CUupDdwp3iFPpTNoLMAwEg" name="marshall-willen-ii-portable-bluetooth-sp-2bd48523-b998-4c85-b830-a731963cc40d.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/CUupDdwp3iFPpTNoLMAwEg.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Willen II is a tiny powerhouse of a speaker that packs quality sound and good battery life into a compact package. It'll last for 17 hours, which is longer than some of the larger competitors. This little speaker is well worth the pickup.<a class="view-deal button" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow" data-dimension112="ebde716e-cea6-4f2b-bf87-0ca2c308112c" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89">View Deal</a></p></div><h2 id="tribit-stormbox-lava">Tribit Stormbox Lava</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RiKpvQVuXrCbdsWTcSLvm" name="Tribit StormBox Lava Bluetooth Speaker" alt="Tribit StormBox Lava Bluetooth Speaker photographed in front of a blue background." src="https://cdn.mos.cms.futurecdn.net/RiKpvQVuXrCbdsWTcSLvm.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Another choice from Tribit, but it's for good reason — the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review">Tribit Stormbox Lava</a> provides incredible ground-shaking bass. This is the speaker that I used almost every single day in my backyard last summer, and it was the perfect addition to every barbecue, bonfire and sunbathing session. </p><p>The 24 hours of battery keep me company all day long, and just like the PocketGo, it's waterproof and dustproof, so there's no need to worry about any spontaneous rain. You can also use stereo pairing with this speaker, but you'll need to purchase two of them.</p><div class="product"><a data-dimension112="44486f2e-5f27-48e3-be22-1ba9709591d8" data-action="Deal Block" data-label="Stormbox Lava" data-dimension48="Stormbox Lava" data-dimension25="$111" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="vbCmBfAuxEssM9rj67FzbM" name="tribit-stormbox-lava-portable-bluetooth--034b079e-af40-4a16-bcae-8001aa3bff5f.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/vbCmBfAuxEssM9rj67FzbM.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Tribit StormBox Lava is a Bluetooth speaker that won't break the bank but still packs a punch. The BassX setting really brings out the bass in your tunes.. It's also tailor-made for the outdoors, with a completely waterproof and dustproof design and up to 24 hours of battery life.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2" target="_blank" rel="nofollow" data-dimension112="44486f2e-5f27-48e3-be22-1ba9709591d8" data-action="Deal Block" data-label="Stormbox Lava" data-dimension48="Stormbox Lava" data-dimension25="$111">View Deal</a></p></div><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/wellness/workouts/stop-doing-100s-of-sit-ups-im-a-personal-trainer-and-i-use-bear-plank-openers-to-build-strong-abs-and-hips-instead"><strong>We know that core workouts build strong ab muscles, but what move should you start with for strength and stability after 50?</strong></a></li><li><a href="https://www.tomsguide.com/wellness/smartwatches/i-walked-5-500-steps-with-the-apple-watch-se-3-vs-garmin-forerunner-70-and-this-smartwatch-won"><strong>I walked 5,500 steps with the Apple Watch SE 3 vs Garmin Forerunner 70 — and the results surprised me</strong></a></li><li><a href="https://www.tomsguide.com/tvs/oled-tvs/we-just-tested-the-lg-b6-oled-and-its-actually-better-than-the-c6-in-this-one-area"><strong>We just lab tested the LG B6 OLED — here's how it stacks up to the LG C6</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/i-test-bluetooth-speakers-for-a-living-and-these-are-the-3-ill-be-using-all-summer-long-and-theyre-all-under-usd140</link>
                                                                            <description>
                            <![CDATA[ Whether you want ground-shaking bass or impressive sound clarity, these are the speakers I recommend for outdoor listening this summer. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">BHAtTqEANQSnhXmHhJBm2X</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/FhkwdSdHD8nTBw8U5o3AiZ-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sun, 24 May 2026 06:15:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ ashley.thieme@futurenet.com (Ashley Thieme) ]]></author>                    <dc:creator><![CDATA[ Ashley Thieme ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/3AWovHjApwuNrSGRS6WBcL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Ashley Thieme is a staff writer on the Reviews team at Tom’s Guide where she tests out the latest tech so you can know what’s going to be worth your time and money. She has a master’s degree in Magazine Journalism and a bachelor’s degree in Journalism, Media and Sociology from Cardiff University. She has bylines in titles including Women’s Health UK, sharing the latest health and wellness news stories, and Virgin Radio UK, providing the latest entertainment news and working on celebrity interviews. She has experience reporting on a variety of topics including music, literature, motorsport, entertainment and health. In previously published work, she has reviewed live music events, books, and wellness products. She values the importance of tech enhancing your life rather than taking over, and as a music fanatic, she is always looking for the best way to listen to new music releases. Discovering the top audio equipment that enhances sound quality and provides optimum comfort is what Ashley does best. In her spare time, Ashley enjoys hitting her reading goals on Goodreads by getting into the latest novels with a cup of tea as well as getting out in the Welsh mountains for a good hike on the weekend.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/FhkwdSdHD8nTBw8U5o3AiZ-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The Tribit PocketGo speaker in black photographed on a blue background. ]]></media:description>                                                            <media:text><![CDATA[The Tribit PocketGo speaker in black photographed on a blue background. ]]></media:text>
                                <media:title type="plain"><![CDATA[The Tribit PocketGo speaker in black photographed on a blue background. ]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/FhkwdSdHD8nTBw8U5o3AiZ-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I test audio gear for a living, and that means I get my hands on some of the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speakers</a> on the market. But in the hours I spend testing these speakers, there are only a few that I keep going back to. </p><p>And these top choice speakers are the proof you need that you don't need to spend big bucks to get awesome sound because they're all on sale. In fact, the priciest one is just $111. </p><p>All of these speakers have impressive battery life, durability protection for the outdoors, and most importantly, impressive sound. So without further ado, these are the speakers I'll be reaching for all summer long. </p><ul><li><strong>Tribit PocketGo: </strong><a href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><strong>was $29 now $19 at Amazon</strong></a></li><li><strong>Marshall Willen II: </strong><a href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow"><strong>was $129 now $89 at Amazon</strong></a><strong></strong></li><li><strong>Tribit Stormbox Lava: </strong><a href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2" target="_blank" rel="nofollow"><strong>was $139 now $111 at Amazon</strong></a></li></ul><h2 id="tribit-pocketgo">Tribit PocketGo</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BzZgxLUemS3J7E73zpJd3a" name="The Tribit PocketGo is the speaker that I will now be bringing with me everywhere, from picnics to hikes. It has awesome sound and can get super loud despite being compact and lightweight. It offers 20 hours of battery life, even with the incredibly loud volumes.  My only qualm with the speaker is that it requires some EQ alterations in the app, depending on the genre you’re listening to. But when it can offer a waterproof rating of IP68 and military-grade drop protection at such a low price, I can’t deny that this is the ultimate adventure speaker." alt="The Tribit PocketGo speaker in black photographed on a blue background." src="https://cdn.mos.cms.futurecdn.net/BzZgxLUemS3J7E73zpJd3a.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-pocketgo-review">Tribit PocketGo</a> is a speaker I use almost daily, and it comes absolutely everywhere with me. In fact, it's coming on vacation with me next week so I can listen to my favorite tracks by the pool. And I don't even have to worry about it getting wet because it has an impressive durability rating of IP68. </p><p>This sub-$30 speaker is nothing short of impressive. It sounds like it should be triple its size when it measures just 4.3 x 3.2 x 1.7 inches. But the 20 hours of battery life are what really win me over. Even at the highest volumes, the battery lasts me days at a time, although this will depend on your own listening habits. </p><div class="product"><a data-dimension112="ca21c58f-b788-4737-a1f4-f1a955d9a872" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$19" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="yFUH5DvQg4Bso6HJiAXHdM" name="tribit-pocketgo-bluetooth-shower-speaker-d7445412-d4ef-4b07-979b-7089fe237b0e.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/yFUH5DvQg4Bso6HJiAXHdM.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Tribit PocketGo is a rugged speaker with an IP68 waterproof rating, making it ideal for adventures, along with military-grade drop protection. After making some minor EQ adjustments in the Tribit app, the speaker sounds great and reaches super loud volumes.<a class="view-deal button" href="https://www.amazon.com/Tribit-Bluetooth-Waterproof-Dustproof-Electronics/dp/B0FQJR6RTS" target="_blank" rel="nofollow" data-dimension112="ca21c58f-b788-4737-a1f4-f1a955d9a872" data-action="Deal Block" data-label="PocketGo" data-dimension48="PocketGo" data-dimension25="$19">View Deal</a></p></div><h2 id="marshall-willen-ii">Marshall Willen II</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dgijJ7akaPXRoD3qRDA2Go" name="Marshall Willen II" alt="Marshall Willen II" src="https://cdn.mos.cms.futurecdn.net/dgijJ7akaPXRoD3qRDA2Go.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>This is the speaker I get out when I want to show off a little. The <a href="https://www.tomsguide.com/audio/marshall-willen-ii-review-rugged-rockin-fun">Marshall Willen II</a> is the sleek answer to the world of portable Bluetooth speakers, and it's not hard to see why. Just look at it. </p><p>It's compact, measuring 3.9 x 3.9 x 1.7 inches, and is as light as a feather, weighing just 0.79 lbs. It's has 17 hours of battery to soundtrack your whole day outdoors without needing to worry about a charger, and the durability rating of IP67 means that the speaker can withstand rain or shine. </p><div class="product"><a data-dimension112="ebde716e-cea6-4f2b-bf87-0ca2c308112c" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="CUupDdwp3iFPpTNoLMAwEg" name="marshall-willen-ii-portable-bluetooth-sp-2bd48523-b998-4c85-b830-a731963cc40d.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/CUupDdwp3iFPpTNoLMAwEg.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Willen II is a tiny powerhouse of a speaker that packs quality sound and good battery life into a compact package. It'll last for 17 hours, which is longer than some of the larger competitors. This little speaker is well worth the pickup.<a class="view-deal button" href="https://www.amazon.com/Marshall-Willen-Bluetooth-Portable-Speaker/dp/B0DD7T8H2Q" target="_blank" rel="nofollow" data-dimension112="ebde716e-cea6-4f2b-bf87-0ca2c308112c" data-action="Deal Block" data-label="Willen II" data-dimension48="Willen II" data-dimension25="$89">View Deal</a></p></div><h2 id="tribit-stormbox-lava">Tribit Stormbox Lava</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RiKpvQVuXrCbdsWTcSLvm" name="Tribit StormBox Lava Bluetooth Speaker" alt="Tribit StormBox Lava Bluetooth Speaker photographed in front of a blue background." src="https://cdn.mos.cms.futurecdn.net/RiKpvQVuXrCbdsWTcSLvm.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Another choice from Tribit, but it's for good reason — the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review">Tribit Stormbox Lava</a> provides incredible ground-shaking bass. This is the speaker that I used almost every single day in my backyard last summer, and it was the perfect addition to every barbecue, bonfire and sunbathing session. </p><p>The 24 hours of battery keep me company all day long, and just like the PocketGo, it's waterproof and dustproof, so there's no need to worry about any spontaneous rain. You can also use stereo pairing with this speaker, but you'll need to purchase two of them.</p><div class="product"><a data-dimension112="44486f2e-5f27-48e3-be22-1ba9709591d8" data-action="Deal Block" data-label="Stormbox Lava" data-dimension48="Stormbox Lava" data-dimension25="$111" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:500px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="vbCmBfAuxEssM9rj67FzbM" name="tribit-stormbox-lava-portable-bluetooth--034b079e-af40-4a16-bcae-8001aa3bff5f.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/vbCmBfAuxEssM9rj67FzbM.jpg" mos="" align="middle" fullscreen="" width="500" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Tribit StormBox Lava is a Bluetooth speaker that won't break the bank but still packs a punch. The BassX setting really brings out the bass in your tunes.. It's also tailor-made for the outdoors, with a completely waterproof and dustproof design and up to 24 hours of battery life.<a class="view-deal button" href="https://www.amazon.com/Tribit-StormBox-Portable-Bluetooth-Waterproof/dp/B0DN5F9BC2" target="_blank" rel="nofollow" data-dimension112="44486f2e-5f27-48e3-be22-1ba9709591d8" data-action="Deal Block" data-label="Stormbox Lava" data-dimension48="Stormbox Lava" data-dimension25="$111">View Deal</a></p></div><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/wellness/workouts/stop-doing-100s-of-sit-ups-im-a-personal-trainer-and-i-use-bear-plank-openers-to-build-strong-abs-and-hips-instead"><strong>We know that core workouts build strong ab muscles, but what move should you start with for strength and stability after 50?</strong></a></li><li><a href="https://www.tomsguide.com/wellness/smartwatches/i-walked-5-500-steps-with-the-apple-watch-se-3-vs-garmin-forerunner-70-and-this-smartwatch-won"><strong>I walked 5,500 steps with the Apple Watch SE 3 vs Garmin Forerunner 70 — and the results surprised me</strong></a></li><li><a href="https://www.tomsguide.com/tvs/oled-tvs/we-just-tested-the-lg-b6-oled-and-its-actually-better-than-the-c6-in-this-one-area"><strong>We just lab tested the LG B6 OLED — here's how it stacks up to the LG C6</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ This Fender Bluetooth speaker doubles as an amp for your guitar — and it’s just $300 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Marshall might be making waves with its audio products like the <a href="https://www.tomsguide.com/audio/headphones/marshall-milton-anc-review"><u>Marshall Milton ANC</u></a>, but it’s not the only instrument maker-turned-electronics manufacturer in town — the Fender brand also has a new suite of Bluetooth speakers that allows you to take your music (and guitars) anywhere and everywhere you want to go. </p><p>The most affordable entry in the two-piece ensemble is the ELIE 6, a $299 portable Bluetooth speaker that was developed by the Fender Audio team. It features a quarter-inch XLR port for music instruments, which I’ve been using with my Ibanez bass.</p><p>Is $300 a bit too much to spend on a <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">portable Bluetooth speaker</a> when you can get louder, more competent models like the LG Xboom Grab and Ultimate Ears Boom 4 for half the price? Yeah, probably. But having a portable speaker that doubles as an amp is certainly handy for vagabonding musicians looking to take their music on the road.</p><div class="product"><a data-dimension112="7b888c9a-5729-4ac2-9b9d-a7646e22e150" data-action="Deal Block" data-label="ELIE 6" data-dimension48="ELIE 6" data-dimension25="$299" href="https://fenderaudio.com/products/elie-06/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:436px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="Fzsfkka34wStY8rzH5T4uN" name="Fender ELIE 6 SQUARE" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Fzsfkka34wStY8rzH5T4uN.jpg" mos="" align="middle" fullscreen="" width="436" height="436" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Need a portable amp that doesn't break your back every time you pick it up? Meet the Fender Audio ELIE 6. This 4-pound speaker delivers strong mids and treble when connected to both Bluetooth devices and music instruments alike. It's IP54 dust protected and water resistant, comes in both Skyscraper Black and Olympic White, and sports an 18-hour battery life. For serenades at the park and impromptu shows for friends, it does the trick.<a class="view-deal button" href="https://fenderaudio.com/products/elie-06/" target="_blank" rel="nofollow" data-dimension112="7b888c9a-5729-4ac2-9b9d-a7646e22e150" data-action="Deal Block" data-label="ELIE 6" data-dimension48="ELIE 6" data-dimension25="$299">View Deal</a></p></div><h2 id="it-s-a-decent-little-bluetooth-speaker">It's a decent little Bluetooth speaker</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="J2jU7sjCQ9oNSX5iFiQkt7" name="Fender-Elie-Speaker-1" alt="A Fender Audio ELIE 6 in a small room." src="https://cdn.mos.cms.futurecdn.net/J2jU7sjCQ9oNSX5iFiQkt7.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Although it doubles as an amp, the ELIE 6 is a Bluetooth speaker first and foremost — and it does a competent job in this role. It doesn’t bring the wall-shaking party the same way the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-4-review">JBL Xtreme 4</a> does, nor does it have the <a href="https://www.tomsguide.com/audio/sony-ult-field-5-review">Sony Ult Field 5’s</a> lightshow, but the sound profile is refined and relatively bassy.  </p><p>In terms of size, it's not all that big. It's only 7.7 x 4.8 x 4.1 inches, so it's easy to throw in a bag. </p><p>To put it through its paces, I used my usual speaker testing playlist that includes songs from Paramore, Eminem, Jimi Hendrix and John Williams. </p><p>By and large, the highs and mids are clear and precise at a moderate volume level and only begin to distort as you push it to 11... I mean full volume. (Sadly there's no <em>actual</em> 11...) </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zdKABwvLHvhBCH9e9TpiG8" name="Fender-Elie-Speaker" alt="A Fender Audio ELIE 6 in a small room." src="https://cdn.mos.cms.futurecdn.net/zdKABwvLHvhBCH9e9TpiG8.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It's bass levels that are very underserved in this speaker, though it's not for a lack of trying. There's a woofer at the bottom of the speaker that does its best to produce low-end frequencies, but the small form factor prevents it from reaching any form of meaty sub-bass.</p><p>If you want to double your output, you can sync up a second ELIE 6 speaker in either stereo, or round up a whole group of speakers if you want to turn your garage into a Guitar Center.</p><p>Personally, the ELIE 6 isn't going to replace my critical listening open-back headphones anytime in the near future, but for small gatherings, it's perfect.</p><h2 id="a-portable-amp-you-ll-want-to-bring-everywhere">A portable amp you'll want to bring everywhere</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5022px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WrCMxkTvoVzt8a3ownkSC9" name="Fender-Elie-Speaker-2" alt="A Fender Audio ELIE 6 in a small room." src="https://cdn.mos.cms.futurecdn.net/WrCMxkTvoVzt8a3ownkSC9.jpg" mos="" align="middle" fullscreen="" width="5022" height="2825" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Whatever you feel about the speaker's sound quality, it's not the whole reason to buy an ELIE 6. You're choosing it over the competition because it doubles as a portable amplifier. </p><p>Now, I’ve never been a road musician, but I’ve carried my fair share of amps for friends and my older brother. Compared to schlepping a traditional 40-pound amplifier around, however, moving the 4-pound ELIE 6 around is a breeze.</p><p>Connecting my Ibanez bass was as simple as plugging in the jack, and volume is controlled by the knob on the right of the controls.</p><div><blockquote><p>There is a very specific kind of person out there that's going to love this speaker. </p></blockquote></div><p>I was able to play a few renditions of my favorite Red Hot Chili Pepper songs, and the sound quality was good. Again, there was no floor-shaking rumble to it, but I have a sneaking suspicion that the amp likely serves guitarists better than bassists. </p><p>Overall, the ELIE 6 is the perfect size and volume level take outside to a park and play for some friends. </p><h2 id="is-the-fender-elie-6-worth-300">Is the Fender ELIE 6 worth $300? </h2><p>There is a very specific kind of person out there that's going to love this speaker. </p><p>It's not the person who wants crystalline-clear sound quality. It's not the person who wants all-day battery life. (Fender says the ELIE 6 is good for about 18 hours of playback, which felt right given my two days of testing.) But it's very appealing for someone who just wants one speaker that doubles as a Bluetooth device and an amp. </p><p>If that's you, rock on. Fender Audio's got you covered.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/im-soundtracking-my-summer-with-the-usd54-jbl-go-5-its-one-of-the-best-budget-speakers-ive-ever-used"><strong>I’m soundtracking my summer with the $54 JBL Go 5 — it’s one of the best budget speakers I’ve ever used</strong></a></li><li><a href="https://www.tomsguide.com/audio/earbuds/i-test-earbuds-for-a-living-and-these-are-the-12-best-cheap-earbuds-you-can-buy-right-now"><strong>I test earbuds for a living and these are the 12 best cheap earbuds you can buy right now</strong></a></li><li><a href="https://www.tomsguide.com/audio/headphones/airpods-max-2-are-my-favorite-over-ear-headphones-but-i-hate-that-i-cant-create-my-own-eq"><strong>AirPods Max 2 are my favorite over-ear headphones — but I hate that I can't create my own EQ</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/this-fender-bluetooth-speaker-doubles-as-an-amp-for-your-guitar-and-its-just-usd300</link>
                                                                            <description>
                            <![CDATA[ This new Bluetooth speaker from Fender allows you to take your music — and guitars — anywhere and everywhere you want to go. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">GjCgUmrpbgCsVJikP2hDPJ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/TwWSnVyjBtMdHvSjxRQ7r6-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 23 May 2026 07:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ Nick.Pino@futurenet.com (Nick Pino) ]]></author>                    <dc:creator><![CDATA[ Nick Pino ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/7xnnEdyK5eEbDVbS5pYB54.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nick Pino heads up the TV and AV verticals at Tom&#039;s Guide and covers everything from OLED TVs to the latest wireless headphones. He&#039;s a Level 1 Certified THX Home Theater Professional, and has attended classes on TV and audio calibration. On Tom&#039;s Guide, he looks after some of the site&#039;s important buying guides like the best TVs, best OLED TVs, best 4K TVs, so most of his day is spent watching and evaluating new screens from LG, Samsung, Sony, Hisense, TCL and Vizio. (And yes, he knows how lucky he is to say that.) He was formerly the Senior Editor, TV and AV at TechRadar (Tom&#039;s Guide&#039;s sister site) and has previously written for GamesRadar, Official Xbox Magazine, PC Gamer and other outlets over the last decade. He got his start on Best Buy&#039;s official video game magazine, @GAMER, for whom he attended his first E3 in 2013. He&#039;s served as a judge for CES&#039; prestigious Innovation Awards and has attended the tech conference in Las Vegas since 2014. He has a computer science degree from the University at Buffalo and still resides in the Buffalo area. (Go Bills!) Not sure which TV you should buy? Drop him an email or tweet him on Twitter and he can help you out.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/TwWSnVyjBtMdHvSjxRQ7r6-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Fender Audio ELIE 6 in a small room.]]></media:description>                                                            <media:text><![CDATA[A Fender Audio ELIE 6 in a small room.]]></media:text>
                                <media:title type="plain"><![CDATA[A Fender Audio ELIE 6 in a small room.]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/TwWSnVyjBtMdHvSjxRQ7r6-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Marshall might be making waves with its audio products like the <a href="https://www.tomsguide.com/audio/headphones/marshall-milton-anc-review"><u>Marshall Milton ANC</u></a>, but it’s not the only instrument maker-turned-electronics manufacturer in town — the Fender brand also has a new suite of Bluetooth speakers that allows you to take your music (and guitars) anywhere and everywhere you want to go. </p><p>The most affordable entry in the two-piece ensemble is the ELIE 6, a $299 portable Bluetooth speaker that was developed by the Fender Audio team. It features a quarter-inch XLR port for music instruments, which I’ve been using with my Ibanez bass.</p><p>Is $300 a bit too much to spend on a <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">portable Bluetooth speaker</a> when you can get louder, more competent models like the LG Xboom Grab and Ultimate Ears Boom 4 for half the price? Yeah, probably. But having a portable speaker that doubles as an amp is certainly handy for vagabonding musicians looking to take their music on the road.</p><div class="product"><a data-dimension112="7b888c9a-5729-4ac2-9b9d-a7646e22e150" data-action="Deal Block" data-label="ELIE 6" data-dimension48="ELIE 6" data-dimension25="$299" href="https://fenderaudio.com/products/elie-06/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:436px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="Fzsfkka34wStY8rzH5T4uN" name="Fender ELIE 6 SQUARE" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Fzsfkka34wStY8rzH5T4uN.jpg" mos="" align="middle" fullscreen="" width="436" height="436" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Need a portable amp that doesn't break your back every time you pick it up? Meet the Fender Audio ELIE 6. This 4-pound speaker delivers strong mids and treble when connected to both Bluetooth devices and music instruments alike. It's IP54 dust protected and water resistant, comes in both Skyscraper Black and Olympic White, and sports an 18-hour battery life. For serenades at the park and impromptu shows for friends, it does the trick.<a class="view-deal button" href="https://fenderaudio.com/products/elie-06/" target="_blank" rel="nofollow" data-dimension112="7b888c9a-5729-4ac2-9b9d-a7646e22e150" data-action="Deal Block" data-label="ELIE 6" data-dimension48="ELIE 6" data-dimension25="$299">View Deal</a></p></div><h2 id="it-s-a-decent-little-bluetooth-speaker">It's a decent little Bluetooth speaker</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="J2jU7sjCQ9oNSX5iFiQkt7" name="Fender-Elie-Speaker-1" alt="A Fender Audio ELIE 6 in a small room." src="https://cdn.mos.cms.futurecdn.net/J2jU7sjCQ9oNSX5iFiQkt7.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Although it doubles as an amp, the ELIE 6 is a Bluetooth speaker first and foremost — and it does a competent job in this role. It doesn’t bring the wall-shaking party the same way the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/jbl-xtreme-4-review">JBL Xtreme 4</a> does, nor does it have the <a href="https://www.tomsguide.com/audio/sony-ult-field-5-review">Sony Ult Field 5’s</a> lightshow, but the sound profile is refined and relatively bassy.  </p><p>In terms of size, it's not all that big. It's only 7.7 x 4.8 x 4.1 inches, so it's easy to throw in a bag. </p><p>To put it through its paces, I used my usual speaker testing playlist that includes songs from Paramore, Eminem, Jimi Hendrix and John Williams. </p><p>By and large, the highs and mids are clear and precise at a moderate volume level and only begin to distort as you push it to 11... I mean full volume. (Sadly there's no <em>actual</em> 11...) </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zdKABwvLHvhBCH9e9TpiG8" name="Fender-Elie-Speaker" alt="A Fender Audio ELIE 6 in a small room." src="https://cdn.mos.cms.futurecdn.net/zdKABwvLHvhBCH9e9TpiG8.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>It's bass levels that are very underserved in this speaker, though it's not for a lack of trying. There's a woofer at the bottom of the speaker that does its best to produce low-end frequencies, but the small form factor prevents it from reaching any form of meaty sub-bass.</p><p>If you want to double your output, you can sync up a second ELIE 6 speaker in either stereo, or round up a whole group of speakers if you want to turn your garage into a Guitar Center.</p><p>Personally, the ELIE 6 isn't going to replace my critical listening open-back headphones anytime in the near future, but for small gatherings, it's perfect.</p><h2 id="a-portable-amp-you-ll-want-to-bring-everywhere">A portable amp you'll want to bring everywhere</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5022px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WrCMxkTvoVzt8a3ownkSC9" name="Fender-Elie-Speaker-2" alt="A Fender Audio ELIE 6 in a small room." src="https://cdn.mos.cms.futurecdn.net/WrCMxkTvoVzt8a3ownkSC9.jpg" mos="" align="middle" fullscreen="" width="5022" height="2825" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Whatever you feel about the speaker's sound quality, it's not the whole reason to buy an ELIE 6. You're choosing it over the competition because it doubles as a portable amplifier. </p><p>Now, I’ve never been a road musician, but I’ve carried my fair share of amps for friends and my older brother. Compared to schlepping a traditional 40-pound amplifier around, however, moving the 4-pound ELIE 6 around is a breeze.</p><p>Connecting my Ibanez bass was as simple as plugging in the jack, and volume is controlled by the knob on the right of the controls.</p><div><blockquote><p>There is a very specific kind of person out there that's going to love this speaker. </p></blockquote></div><p>I was able to play a few renditions of my favorite Red Hot Chili Pepper songs, and the sound quality was good. Again, there was no floor-shaking rumble to it, but I have a sneaking suspicion that the amp likely serves guitarists better than bassists. </p><p>Overall, the ELIE 6 is the perfect size and volume level take outside to a park and play for some friends. </p><h2 id="is-the-fender-elie-6-worth-300">Is the Fender ELIE 6 worth $300? </h2><p>There is a very specific kind of person out there that's going to love this speaker. </p><p>It's not the person who wants crystalline-clear sound quality. It's not the person who wants all-day battery life. (Fender says the ELIE 6 is good for about 18 hours of playback, which felt right given my two days of testing.) But it's very appealing for someone who just wants one speaker that doubles as a Bluetooth device and an amp. </p><p>If that's you, rock on. Fender Audio's got you covered.</p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/im-soundtracking-my-summer-with-the-usd54-jbl-go-5-its-one-of-the-best-budget-speakers-ive-ever-used"><strong>I’m soundtracking my summer with the $54 JBL Go 5 — it’s one of the best budget speakers I’ve ever used</strong></a></li><li><a href="https://www.tomsguide.com/audio/earbuds/i-test-earbuds-for-a-living-and-these-are-the-12-best-cheap-earbuds-you-can-buy-right-now"><strong>I test earbuds for a living and these are the 12 best cheap earbuds you can buy right now</strong></a></li><li><a href="https://www.tomsguide.com/audio/headphones/airpods-max-2-are-my-favorite-over-ear-headphones-but-i-hate-that-i-cant-create-my-own-eq"><strong>AirPods Max 2 are my favorite over-ear headphones — but I hate that I can't create my own EQ</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I've been using this tiny Bluetooth speaker for 3 months — its surprisingly big sound has transformed my home listening (and it's on sale right now) ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Here at Tom's Guide, we're committed to helping you <a href="https://www.tomsguide.com/savings-squad">upgrade your life for less</a> by finding gadgets that can make everything a little easier without breaking the bank. And that includes making sure you have the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> on hand for summer parties and personal listening. </p><p>If you haven't already, now's the time to get your hands on an audio upgrade for the summer, and I'm here to tell you that it doesn't have to be from one of the most expensive brands like Sonos or Bose. You can find decent audio performance for well under $100; you just have to know where to look. </p><p>One of my top recommendations for this summer is the <a href="https://www.amazon.com/Edifier-Bluetooth-Waterproof-Dustproof-Connectivity/dp/B0DTHMKKGR" target="_blank" rel="nofollow">Edifier ES20, which is currently on sale at Amazon for $69</a>. It's small but mighty, and is a perfect option for outdoor gatherings. Here are my favorite things about the speaker. </p><div class="product"><a data-dimension112="6bdc090c-4fda-4aff-ae53-c5f0781685e5" data-action="Deal Block" data-label="ES20" data-dimension48="ES20" data-dimension25="$69" href="https://www.amazon.com/Edifier-Bluetooth-Waterproof-Dustproof-Connectivity/dp/B0DTHMKKGR" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:444px;"><p class="vanilla-image-block" style="padding-top:112.61%;"><img id="NU43UEyEPMFupBtxZuNuJ6" name="edifier-es20-portable-bluetooth-speaker--9961544e-fd18-41de-b929-b92bed2e028c.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/NU43UEyEPMFupBtxZuNuJ6.jpg" mos="" align="middle" fullscreen="" width="444" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/edifier-es20-review" data-dimension112="6bdc090c-4fda-4aff-ae53-c5f0781685e5" data-action="Deal Block" data-label="ES20" data-dimension48="ES20" data-dimension25="$69">Edifier ES20</a> is a powerful, stylish speaker with remarkably loud output — it seriously shocked me. The speaker has a durability rating of IP67,  and has a stereo pairing option for bigger sound. I’d recommend it to the more budget-conscious among you who still want great sound performance.<a class="view-deal button" href="https://www.amazon.com/Edifier-Bluetooth-Waterproof-Dustproof-Connectivity/dp/B0DTHMKKGR" target="_blank" rel="nofollow" data-dimension112="6bdc090c-4fda-4aff-ae53-c5f0781685e5" data-action="Deal Block" data-label="ES20" data-dimension48="ES20" data-dimension25="$69">View Deal</a></p></div><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xp4N4X"></div>                            </div>                            <script src="https://kwizly.com/embed/Xp4N4X.js" async></script><h2 id="premium-and-weatherproof-design">Premium and weatherproof design </h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nKgLaXwrZFj2H8MgvASwNG" name="Edifier ES20" alt="Edifier ES20 Bluetooth speaker in ivory colorway photographed on a blue background" src="https://cdn.mos.cms.futurecdn.net/nKgLaXwrZFj2H8MgvASwNG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Edifier ES20 is one good-looking speaker. It comes in ivory or black, and both versions of the speaker look premium with champagne accents and a metal grille on the front, which is reminiscent of the <a href="https://www.tomsguide.com/audio/marshall-willen-ii-review-rugged-rockin-fun">Marshall Willen II</a>. </p><p>It's a very small speaker, measuring just 3.7 x 3.6 x 2.0 inches, but don't let that put you off because its sound is larger than life. The size makes it easily portable, so it can go absolutely anywhere.</p><p>Beyond the good looks, there is functionality too. The speaker has an IP67 rating, meaning it's fully protected against dust and can even be submerged underwater. So there's no fear if it happens to fall into the pool by mistake!</p><h2 id="it-gets-very-loud">It gets very loud</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hKXzdWbfyxAswY3SdqeAJG" name="Edifier ES20" alt="Edifier ES20 Bluetooth speaker in ivory colorway photographed on a blue background" src="https://cdn.mos.cms.futurecdn.net/hKXzdWbfyxAswY3SdqeAJG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As I was saying, don't let that small size deter you. This speaker can get incredibly loud — I was seriously shocked. When using the speaker indoors, I could hear it clearly from the other side of my house. And the sound carries well outside, too.</p><p>It rivals some bigger boombox-style speakers I've tested in the past, such as the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review">Tribit Stormbox Lava</a>. At the lowest volumes, the Edifier starts to struggle with a little static in the bass, but that's to be expected. </p><h2 id="and-you-can-go-louder">And you can go louder</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sVLerSfHyqgZUUfV6UnvdG" name="Edifier ES20" alt="Edifier ES20 Bluetooth speaker in ivory colorway photographed on a blue background" src="https://cdn.mos.cms.futurecdn.net/sVLerSfHyqgZUUfV6UnvdG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Speaking of loud, the Edifier ES20 has stereo pairing capabilities. That means if you have more than one of the speakers (and at that price, why wouldn't you?), then you can pair them to create a surround sound setup. </p><p>This can create a more immersive sound for casual listening too, with a wider sound stage to support instrument separation and avoid static. </p><p>This is a speaker I'm going to be reaching for all summer long, but how about you? If you like the sound of this small but mighty speaker, you can read my full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/edifier-es20-review">Edifier ES20 review</a> to find out more. </p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/edifier-es20-review#section-edifier-es20-review-sound-quality">This budget speaker looks totally premium — and it almost nails it in the sound department too</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/expensive-doesnt-mean-good-heres-why-ill-be-sticking-with-my-usd40-speaker-over-this-usd200-one">'Expensive doesn’t mean good’: here’s why I’ll be sticking with my $40 speaker over this $200 one</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/you-dont-have-to-buy-the-newest-or-most-expensive-bluetooth-speaker-for-awesome-sound-here-are-my-top-picks-for-any-occasion-that-wont-break-the-bank">You don't have to buy the most expensive Bluetooth speaker for awesome sound — here are my top picks under $30, $50 and $100</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/bluetooth-speakers/ive-been-using-this-tiny-speaker-for-3-months-and-its-surprisingly-big-sound-has-transformed-my-home-listening-and-its-on-sale-right-now</link>
                                                                            <description>
                            <![CDATA[ The Edifier ES20 is a tiny speaker that can withstand the elements and provide big sound, and it's currently on sale for a limited time. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">MmzBPYWuUuUTmo4oeFMgdA</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/KEmfsyHMnP7fbFHFMzw7Jg-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 16 May 2026 09:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Bluetooth Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ ashley.thieme@futurenet.com (Ashley Thieme) ]]></author>                    <dc:creator><![CDATA[ Ashley Thieme ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/3AWovHjApwuNrSGRS6WBcL.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Ashley Thieme is a staff writer on the Reviews team at Tom’s Guide where she tests out the latest tech so you can know what’s going to be worth your time and money. She has a master’s degree in Magazine Journalism and a bachelor’s degree in Journalism, Media and Sociology from Cardiff University. She has bylines in titles including Women’s Health UK, sharing the latest health and wellness news stories, and Virgin Radio UK, providing the latest entertainment news and working on celebrity interviews. She has experience reporting on a variety of topics including music, literature, motorsport, entertainment and health. In previously published work, she has reviewed live music events, books, and wellness products. She values the importance of tech enhancing your life rather than taking over, and as a music fanatic, she is always looking for the best way to listen to new music releases. Discovering the top audio equipment that enhances sound quality and provides optimum comfort is what Ashley does best. In her spare time, Ashley enjoys hitting her reading goals on Goodreads by getting into the latest novels with a cup of tea as well as getting out in the Welsh mountains for a good hike on the weekend.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/KEmfsyHMnP7fbFHFMzw7Jg-1280-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Edifier ES20 Bluetooth speaker in ivory photographed on a gray table in front of a blue background. ]]></media:description>                                                            <media:text><![CDATA[Edifier ES20 Bluetooth speaker in ivory photographed on a gray table in front of a blue background. ]]></media:text>
                                <media:title type="plain"><![CDATA[Edifier ES20 Bluetooth speaker in ivory photographed on a gray table in front of a blue background. ]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/KEmfsyHMnP7fbFHFMzw7Jg-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Here at Tom's Guide, we're committed to helping you <a href="https://www.tomsguide.com/savings-squad">upgrade your life for less</a> by finding gadgets that can make everything a little easier without breaking the bank. And that includes making sure you have the <a href="https://www.tomsguide.com/us/best-bluetooth-speakers,review-1982.html">best Bluetooth speaker</a> on hand for summer parties and personal listening. </p><p>If you haven't already, now's the time to get your hands on an audio upgrade for the summer, and I'm here to tell you that it doesn't have to be from one of the most expensive brands like Sonos or Bose. You can find decent audio performance for well under $100; you just have to know where to look. </p><p>One of my top recommendations for this summer is the <a href="https://www.amazon.com/Edifier-Bluetooth-Waterproof-Dustproof-Connectivity/dp/B0DTHMKKGR" target="_blank" rel="nofollow">Edifier ES20, which is currently on sale at Amazon for $69</a>. It's small but mighty, and is a perfect option for outdoor gatherings. Here are my favorite things about the speaker. </p><div class="product"><a data-dimension112="6bdc090c-4fda-4aff-ae53-c5f0781685e5" data-action="Deal Block" data-label="ES20" data-dimension48="ES20" data-dimension25="$69" href="https://www.amazon.com/Edifier-Bluetooth-Waterproof-Dustproof-Connectivity/dp/B0DTHMKKGR" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:444px;"><p class="vanilla-image-block" style="padding-top:112.61%;"><img id="NU43UEyEPMFupBtxZuNuJ6" name="edifier-es20-portable-bluetooth-speaker--9961544e-fd18-41de-b929-b92bed2e028c.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/NU43UEyEPMFupBtxZuNuJ6.jpg" mos="" align="middle" fullscreen="" width="444" height="500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/audio/bluetooth-speakers/edifier-es20-review" data-dimension112="6bdc090c-4fda-4aff-ae53-c5f0781685e5" data-action="Deal Block" data-label="ES20" data-dimension48="ES20" data-dimension25="$69">Edifier ES20</a> is a powerful, stylish speaker with remarkably loud output — it seriously shocked me. The speaker has a durability rating of IP67,  and has a stereo pairing option for bigger sound. I’d recommend it to the more budget-conscious among you who still want great sound performance.<a class="view-deal button" href="https://www.amazon.com/Edifier-Bluetooth-Waterproof-Dustproof-Connectivity/dp/B0DTHMKKGR" target="_blank" rel="nofollow" data-dimension112="6bdc090c-4fda-4aff-ae53-c5f0781685e5" data-action="Deal Block" data-label="ES20" data-dimension48="ES20" data-dimension25="$69">View Deal</a></p></div><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xp4N4X"></div>                            </div>                            <script src="https://kwizly.com/embed/Xp4N4X.js" async></script><h2 id="premium-and-weatherproof-design">Premium and weatherproof design </h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nKgLaXwrZFj2H8MgvASwNG" name="Edifier ES20" alt="Edifier ES20 Bluetooth speaker in ivory colorway photographed on a blue background" src="https://cdn.mos.cms.futurecdn.net/nKgLaXwrZFj2H8MgvASwNG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The Edifier ES20 is one good-looking speaker. It comes in ivory or black, and both versions of the speaker look premium with champagne accents and a metal grille on the front, which is reminiscent of the <a href="https://www.tomsguide.com/audio/marshall-willen-ii-review-rugged-rockin-fun">Marshall Willen II</a>. </p><p>It's a very small speaker, measuring just 3.7 x 3.6 x 2.0 inches, but don't let that put you off because its sound is larger than life. The size makes it easily portable, so it can go absolutely anywhere.</p><p>Beyond the good looks, there is functionality too. The speaker has an IP67 rating, meaning it's fully protected against dust and can even be submerged underwater. So there's no fear if it happens to fall into the pool by mistake!</p><h2 id="it-gets-very-loud">It gets very loud</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hKXzdWbfyxAswY3SdqeAJG" name="Edifier ES20" alt="Edifier ES20 Bluetooth speaker in ivory colorway photographed on a blue background" src="https://cdn.mos.cms.futurecdn.net/hKXzdWbfyxAswY3SdqeAJG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>As I was saying, don't let that small size deter you. This speaker can get incredibly loud — I was seriously shocked. When using the speaker indoors, I could hear it clearly from the other side of my house. And the sound carries well outside, too.</p><p>It rivals some bigger boombox-style speakers I've tested in the past, such as the <a href="https://www.tomsguide.com/audio/bluetooth-speakers/tribit-stormbox-lava-review">Tribit Stormbox Lava</a>. At the lowest volumes, the Edifier starts to struggle with a little static in the bass, but that's to be expected. </p><h2 id="and-you-can-go-louder">And you can go louder</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="sVLerSfHyqgZUUfV6UnvdG" name="Edifier ES20" alt="Edifier ES20 Bluetooth speaker in ivory colorway photographed on a blue background" src="https://cdn.mos.cms.futurecdn.net/sVLerSfHyqgZUUfV6UnvdG.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Speaking of loud, the Edifier ES20 has stereo pairing capabilities. That means if you have more than one of the speakers (and at that price, why wouldn't you?), then you can pair them to create a surround sound setup. </p><p>This can create a more immersive sound for casual listening too, with a wider sound stage to support instrument separation and avoid static. </p><p>This is a speaker I'm going to be reaching for all summer long, but how about you? If you like the sound of this small but mighty speaker, you can read my full <a href="https://www.tomsguide.com/audio/bluetooth-speakers/edifier-es20-review">Edifier ES20 review</a> to find out more. </p><h3 class="article-body__section" id="section-more-from-tom-s-guide"><span>More from Tom's Guide</span></h3><ul><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/edifier-es20-review#section-edifier-es20-review-sound-quality">This budget speaker looks totally premium — and it almost nails it in the sound department too</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/expensive-doesnt-mean-good-heres-why-ill-be-sticking-with-my-usd40-speaker-over-this-usd200-one">'Expensive doesn’t mean good’: here’s why I’ll be sticking with my $40 speaker over this $200 one</a></li><li><a href="https://www.tomsguide.com/audio/bluetooth-speakers/you-dont-have-to-buy-the-newest-or-most-expensive-bluetooth-speaker-for-awesome-sound-here-are-my-top-picks-for-any-occasion-that-wont-break-the-bank">You don't have to buy the most expensive Bluetooth speaker for awesome sound — here are my top picks under $30, $50 and $100</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Bose Lifestyle Ultra Soundbar hands-on review: Finally, a real Sonos Arc Ultra rival ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Bose has shown us time and time again that it can build some truly great soundbars — both the <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review">Bose Smart</a> and <a href="https://www.tomsguide.com/reviews/bose-smart-ultra-soundbar">Bose Smart Ultra</a> soundbars being good examples. But in 2026, the brand wants to take things to another level with the Bose Lifestyle Ultra Soundbar.</p><p>Although I’ve only had a few days with it, the Bose Lifestyle Ultra Soundbar feels like it’s a true <a href="https://www.tomsguide.com/audio/soundbars/sonos-arc-ultra-review">Sonos Arc Ultra</a> competitor. It sounds great both with and without the additional Lifestyle Ultra Subwoofer, and I was hard-pressed to hear anything wrong with it. </p><p>Is it time for the Arc Ultra to finally give up its crown as the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbar</a>? </p><h3 class="article-body__section" id="section-tl-dr"><span>TL;DR</span></h3><ul><li><strong>What is it?</strong> A new Spatial Audio soundbar from Bose</li><li><strong>How much will it cost?</strong> $1,099 for the black or white model</li><li><strong>What does it work with?</strong> Any streaming service that supports Google Cast, Apple AirPlay, Spotify Connect, Wi-Fi, or Bluetooth</li></ul><h3 class="article-body__section" id="section-price-and-availability"><span>Price and availability</span></h3><div  class="fancy-box"><div class="fancy_box-title">Bose LifeStyle Ultra Speaker: Specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price: </strong>$1,099<br><strong>Speakers: </strong>9 total speakers and center tweeter<br><strong>Supported streaming:</strong> Google Cast, Apple AirPlay, Spotify Connect<br><strong>Connectivity:</strong> Wi-Fi 6, Bluetooth 5.3<br><strong>Size: </strong>43.54 x 2.64 x 4.96 inches</p></div></div><p>The Bose Lifestyle Ultra Soundbar was announced on May 5 2026, and became available to pre-order at <a href="https://www.bose.com/p/home-theater/bose-lifestyle-ultra-soundbar/LSULT-SOUNDBAR.html" target="_blank" rel="nofollow">Bose.com</a> shortly afterwards. </p><p>At $1,099, it’s the most expensive soundbar in Bose’s range, but it’s similar in specs and power to the $999 Bose Smart Ultra Soundbar and is exactly the same price as its closest competitor, the Sonos Arc Ultra.</p><p>If you want something a bit more manageable in price, the 3.0-channel Bose TV Soundbar starts at $279, while the 5.0-channel Bose Smart Soundbar can be had for $549.</p><div data-widget-type="review" data-model-name="Bose Lifestyle Ultra Soundbar"></div><h3 class="article-body__section" id="section-design"><span>Design</span></h3><p>Bose being Bose, the Lifestyle Ultra Soundbar only comes in two color options: black and white… Excuse me, <em>Arctic White</em>.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Y56bswcNo4bqVxx8wBuEUM" name="Bose Lifestyle Ultra Soundbar" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/Y56bswcNo4bqVxx8wBuEUM.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The top of the soundbar is divided into two layers. There’s a fabric layer that wraps all the way around the front and sides of the soundbar and a glossy section that sits on top and acts almost as a mirror to the TV. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3913px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FvvJaAwm6BixTvHpbrC4mM" name="Bose Lifestyle Ultra Soundbar-1" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/FvvJaAwm6BixTvHpbrC4mM.jpg" mos="" align="middle" fullscreen="" width="3913" height="2201" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Spin it around to the back and you'll see a strange array of ports. You've got the essential HDMI-in port that you'll want to connect to the eARC HDMI port on your TV, but also connections for Ethernet, Bass and Data, as well as a USB-C port for service. </p><p>I'm not sure I totally understand why some of these ports are here and I would trade any one of the aforementioned extras for an HDMI passthrough that could link up to a Blu-ray player or game console. </p><p>On the top right of the soundbar are the touch-sensitive controls laid out in a large circular indentation. You'll mostly use your phone to control playback, but these are handy to use in a pinch. </p><h3 class="article-body__section" id="section-audio"><span>Audio </span></h3><p>According to Bose, the speaker features six full-range drivers (two up-firing and four front-facing) alongside an additional center tweeter and two proprietary Bose PhaseGuide drivers inside the main bar. On the back are two QuietPort acoustic openings that enable what Bose calls "CleanBass" technology. </p><p>The specs are all very impressive on paper. But how does the speaker actually sound? I haven’t put the Bose Lifestyle Ultra Soundbar through my entire testing regimen yet, but I’m fairly pleased with what I’ve heard from it so far.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="k5e9YrtmUXQehy84sCQtvM" name="Bose Lifestyle Ultra Soundbar-2" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/k5e9YrtmUXQehy84sCQtvM.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To kick things off, I started with songs from Qobuz, my streaming service of choice. First up: “The Mountain” by Gorillaz. Its blend of sitars and flutes fluttered wonderfully out of the soundbar, never too shrill for my ears and perfectly balanced. The soundstage wasn’t the largest, especially if I toggled off the subwoofer, but the clarity was great. </p><p>To test the bass response, I threw on “Barbra Streisand” by Duck Sauce. If you need an impromptu dance party in your living room, I highly recommend giving this a listen. The bass response wasn’t the greatest with just the soundbar, but there was enough oomph there to get me bobbing along with the beat. </p><p>Testing some classical music, “Star Wars: A New Hope Main Title” was up next — and I knew this one was going to be difficult for the soundbar. </p><p>Unfortunately, to make classical music to sound like you’re listening to it at the philharmonic, you <em>need </em>a wide soundstage. It’s here the Bose Lifestyle Ultra Soundbar fell a little flat. There was nothing wrong with the sound profile itself, mind you, it just felt cramped. Admittedly, this is an issue with nearly all soundbars, but I was hoping Bose’s CustomTune technology would widen things a bit. </p><p>Last but not least was “INDUSTRY BABY” by Lil Nas X, a perennial banger that should have the windows and walls shaking with its deep sub-bass. Without the subwoofer enabled, the Bose Lifestyle Ultra Soundbar got pretty darn low, but it wasn’t until I flipped back on the subwoofer that I really got the walls to shake. </p><p>To see how dialogue sounded, I threw on a Premiere League game. With the SpeechClarity setting at Medium, the announcers were crystal clear and the cheers of the audience were audible, but not overwhelming. This is an area that I'll really dive into with more movies and shows when I write the final review early next week.</p><h3 class="article-body__section" id="section-setup-and-smart-features"><span>Setup and smart features</span></h3><p>Setup for the Lifestyle Ultra Soundbar is done via the Bose app. The whole process can be done in just a few minutes and requires little technical knowledge to pull off. It also worked the first time, which is something I can’t say about every soundbar I’ve tested recently (cough, LG Sound Suite H7, cough). </p><p>Inside the app are a number of very simple tuning options. There’s a three-band EQ (treble, mids, bass) and adjustments for height and surround. You’ll want to adjust the height of the sound to match the distance to your TV. If the soundbar sits five feet below the TV, crank this setting up to +5. Need the sound lower? Go in the other direction to -5.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="XMas2Rc3chLFYifzVuDn6L" name="Bose Lifestyle Ultra Soundbar LIST2" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/XMas2Rc3chLFYifzVuDn6L.jpg" mos="" align="middle" fullscreen="" width="1500" height="844" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Bose)</span></figcaption></figure><p>If you pick up the subwoofer to go with it, you’ll be able to toggle it on and off via the app as well as increase its output if you feel the bass is a little lacking. I had mine set to a +2 for the majority of my hands-on time, but I occasionally cranked it up higher to see if I could rattle windows. (I was able to and my wife was none-too-pleased about it.)  </p><p>You’ll also come into the app to re-configure your setup if you shell out for a pair of <a href="https://www.tomsguide.com/audio/smart-speakers/bose-lifestyle-ultra-speaker-review">Bose Lifestyle Ultra speakers</a> or need to change the source of your playback from HDMI to Bluetooth or AirPlay. </p><h3 class="article-body__section" id="section-outlook"><span>Outlook</span></h3><p>I'll need to spend some more time with it before I can say definitively whether this overtakes the Sonos Arc Ultra, but I think it's going to be exceptionally close. </p><p>Features like Bose's custom tuning and SpeechClarity are genuinely helpful, and I think music sounds fantastic for a soundbar. There are some minor issues like the small soundstage and lack of an HDMI passthrough port that are troubling, however.</p><p>Although it's a bit pricey, the Lifestyle Ultra is a darn good entry in Bose's storied soundbar lineup, and it's one I'm excited to fully test over the next week or two. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/soundbars/bose-lifestyle-ultra-soundbar-hands-on-review-finally-a-real-sonos-arc-ultra-rival</link>
                                                                            <description>
                            <![CDATA[ Although I’ve only had a few days with it so far, the Bose Lifestyle Ultra Soundbar definitely feels like it’s a true Sonos Arc Ultra competitor. Is it time for the Arc Ultra to finally give up its crown as the best soundbar? ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">it9DXiPHRqkjDuhbFXxMXa</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/Mc5DuPc8HGANrHARLmE7FL-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 16 May 2026 07:15:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Soundbars]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ Nick.Pino@futurenet.com (Nick Pino) ]]></author>                    <dc:creator><![CDATA[ Nick Pino ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/7xnnEdyK5eEbDVbS5pYB54.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nick Pino heads up the TV and AV verticals at Tom&#039;s Guide and covers everything from OLED TVs to the latest wireless headphones. He&#039;s a Level 1 Certified THX Home Theater Professional, and has attended classes on TV and audio calibration. On Tom&#039;s Guide, he looks after some of the site&#039;s important buying guides like the best TVs, best OLED TVs, best 4K TVs, so most of his day is spent watching and evaluating new screens from LG, Samsung, Sony, Hisense, TCL and Vizio. (And yes, he knows how lucky he is to say that.) He was formerly the Senior Editor, TV and AV at TechRadar (Tom&#039;s Guide&#039;s sister site) and has previously written for GamesRadar, Official Xbox Magazine, PC Gamer and other outlets over the last decade. He got his start on Best Buy&#039;s official video game magazine, @GAMER, for whom he attended his first E3 in 2013. He&#039;s served as a judge for CES&#039; prestigious Innovation Awards and has attended the tech conference in Las Vegas since 2014. He has a computer science degree from the University at Buffalo and still resides in the Buffalo area. (Go Bills!) Not sure which TV you should buy? Drop him an email or tweet him on Twitter and he can help you out.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/Mc5DuPc8HGANrHARLmE7FL-1280-80.jpg">
                                                            <media:credit><![CDATA[Bose]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Bose Lifestyle Ultra Soundbar shown in living room]]></media:description>                                                            <media:text><![CDATA[Bose Lifestyle Ultra Soundbar shown in living room]]></media:text>
                                <media:title type="plain"><![CDATA[Bose Lifestyle Ultra Soundbar shown in living room]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/Mc5DuPc8HGANrHARLmE7FL-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Bose has shown us time and time again that it can build some truly great soundbars — both the <a href="https://www.tomsguide.com/audio/soundbars/bose-smart-soundbar-review">Bose Smart</a> and <a href="https://www.tomsguide.com/reviews/bose-smart-ultra-soundbar">Bose Smart Ultra</a> soundbars being good examples. But in 2026, the brand wants to take things to another level with the Bose Lifestyle Ultra Soundbar.</p><p>Although I’ve only had a few days with it, the Bose Lifestyle Ultra Soundbar feels like it’s a true <a href="https://www.tomsguide.com/audio/soundbars/sonos-arc-ultra-review">Sonos Arc Ultra</a> competitor. It sounds great both with and without the additional Lifestyle Ultra Subwoofer, and I was hard-pressed to hear anything wrong with it. </p><p>Is it time for the Arc Ultra to finally give up its crown as the <a href="https://www.tomsguide.com/audio/soundbars/best-soundbars">best soundbar</a>? </p><h3 class="article-body__section" id="section-tl-dr"><span>TL;DR</span></h3><ul><li><strong>What is it?</strong> A new Spatial Audio soundbar from Bose</li><li><strong>How much will it cost?</strong> $1,099 for the black or white model</li><li><strong>What does it work with?</strong> Any streaming service that supports Google Cast, Apple AirPlay, Spotify Connect, Wi-Fi, or Bluetooth</li></ul><h3 class="article-body__section" id="section-price-and-availability"><span>Price and availability</span></h3><div  class="fancy-box"><div class="fancy_box-title">Bose LifeStyle Ultra Speaker: Specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price: </strong>$1,099<br><strong>Speakers: </strong>9 total speakers and center tweeter<br><strong>Supported streaming:</strong> Google Cast, Apple AirPlay, Spotify Connect<br><strong>Connectivity:</strong> Wi-Fi 6, Bluetooth 5.3<br><strong>Size: </strong>43.54 x 2.64 x 4.96 inches</p></div></div><p>The Bose Lifestyle Ultra Soundbar was announced on May 5 2026, and became available to pre-order at <a href="https://www.bose.com/p/home-theater/bose-lifestyle-ultra-soundbar/LSULT-SOUNDBAR.html" target="_blank" rel="nofollow">Bose.com</a> shortly afterwards. </p><p>At $1,099, it’s the most expensive soundbar in Bose’s range, but it’s similar in specs and power to the $999 Bose Smart Ultra Soundbar and is exactly the same price as its closest competitor, the Sonos Arc Ultra.</p><p>If you want something a bit more manageable in price, the 3.0-channel Bose TV Soundbar starts at $279, while the 5.0-channel Bose Smart Soundbar can be had for $549.</p><div data-widget-type="review" data-model-name="Bose Lifestyle Ultra Soundbar"></div><h3 class="article-body__section" id="section-design"><span>Design</span></h3><p>Bose being Bose, the Lifestyle Ultra Soundbar only comes in two color options: black and white… Excuse me, <em>Arctic White</em>.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Y56bswcNo4bqVxx8wBuEUM" name="Bose Lifestyle Ultra Soundbar" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/Y56bswcNo4bqVxx8wBuEUM.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>The top of the soundbar is divided into two layers. There’s a fabric layer that wraps all the way around the front and sides of the soundbar and a glossy section that sits on top and acts almost as a mirror to the TV. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3913px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FvvJaAwm6BixTvHpbrC4mM" name="Bose Lifestyle Ultra Soundbar-1" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/FvvJaAwm6BixTvHpbrC4mM.jpg" mos="" align="middle" fullscreen="" width="3913" height="2201" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Spin it around to the back and you'll see a strange array of ports. You've got the essential HDMI-in port that you'll want to connect to the eARC HDMI port on your TV, but also connections for Ethernet, Bass and Data, as well as a USB-C port for service. </p><p>I'm not sure I totally understand why some of these ports are here and I would trade any one of the aforementioned extras for an HDMI passthrough that could link up to a Blu-ray player or game console. </p><p>On the top right of the soundbar are the touch-sensitive controls laid out in a large circular indentation. You'll mostly use your phone to control playback, but these are handy to use in a pinch. </p><h3 class="article-body__section" id="section-audio"><span>Audio </span></h3><p>According to Bose, the speaker features six full-range drivers (two up-firing and four front-facing) alongside an additional center tweeter and two proprietary Bose PhaseGuide drivers inside the main bar. On the back are two QuietPort acoustic openings that enable what Bose calls "CleanBass" technology. </p><p>The specs are all very impressive on paper. But how does the speaker actually sound? I haven’t put the Bose Lifestyle Ultra Soundbar through my entire testing regimen yet, but I’m fairly pleased with what I’ve heard from it so far.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4032px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="k5e9YrtmUXQehy84sCQtvM" name="Bose Lifestyle Ultra Soundbar-2" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/k5e9YrtmUXQehy84sCQtvM.jpg" mos="" align="middle" fullscreen="" width="4032" height="2268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>To kick things off, I started with songs from Qobuz, my streaming service of choice. First up: “The Mountain” by Gorillaz. Its blend of sitars and flutes fluttered wonderfully out of the soundbar, never too shrill for my ears and perfectly balanced. The soundstage wasn’t the largest, especially if I toggled off the subwoofer, but the clarity was great. </p><p>To test the bass response, I threw on “Barbra Streisand” by Duck Sauce. If you need an impromptu dance party in your living room, I highly recommend giving this a listen. The bass response wasn’t the greatest with just the soundbar, but there was enough oomph there to get me bobbing along with the beat. </p><p>Testing some classical music, “Star Wars: A New Hope Main Title” was up next — and I knew this one was going to be difficult for the soundbar. </p><p>Unfortunately, to make classical music to sound like you’re listening to it at the philharmonic, you <em>need </em>a wide soundstage. It’s here the Bose Lifestyle Ultra Soundbar fell a little flat. There was nothing wrong with the sound profile itself, mind you, it just felt cramped. Admittedly, this is an issue with nearly all soundbars, but I was hoping Bose’s CustomTune technology would widen things a bit. </p><p>Last but not least was “INDUSTRY BABY” by Lil Nas X, a perennial banger that should have the windows and walls shaking with its deep sub-bass. Without the subwoofer enabled, the Bose Lifestyle Ultra Soundbar got pretty darn low, but it wasn’t until I flipped back on the subwoofer that I really got the walls to shake. </p><p>To see how dialogue sounded, I threw on a Premiere League game. With the SpeechClarity setting at Medium, the announcers were crystal clear and the cheers of the audience were audible, but not overwhelming. This is an area that I'll really dive into with more movies and shows when I write the final review early next week.</p><h3 class="article-body__section" id="section-setup-and-smart-features"><span>Setup and smart features</span></h3><p>Setup for the Lifestyle Ultra Soundbar is done via the Bose app. The whole process can be done in just a few minutes and requires little technical knowledge to pull off. It also worked the first time, which is something I can’t say about every soundbar I’ve tested recently (cough, LG Sound Suite H7, cough). </p><p>Inside the app are a number of very simple tuning options. There’s a three-band EQ (treble, mids, bass) and adjustments for height and surround. You’ll want to adjust the height of the sound to match the distance to your TV. If the soundbar sits five feet below the TV, crank this setting up to +5. Need the sound lower? Go in the other direction to -5.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1500px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="XMas2Rc3chLFYifzVuDn6L" name="Bose Lifestyle Ultra Soundbar LIST2" alt="Bose Lifestyle Ultra Soundbar shown in living room" src="https://cdn.mos.cms.futurecdn.net/XMas2Rc3chLFYifzVuDn6L.jpg" mos="" align="middle" fullscreen="" width="1500" height="844" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Bose)</span></figcaption></figure><p>If you pick up the subwoofer to go with it, you’ll be able to toggle it on and off via the app as well as increase its output if you feel the bass is a little lacking. I had mine set to a +2 for the majority of my hands-on time, but I occasionally cranked it up higher to see if I could rattle windows. (I was able to and my wife was none-too-pleased about it.)  </p><p>You’ll also come into the app to re-configure your setup if you shell out for a pair of <a href="https://www.tomsguide.com/audio/smart-speakers/bose-lifestyle-ultra-speaker-review">Bose Lifestyle Ultra speakers</a> or need to change the source of your playback from HDMI to Bluetooth or AirPlay. </p><h3 class="article-body__section" id="section-outlook"><span>Outlook</span></h3><p>I'll need to spend some more time with it before I can say definitively whether this overtakes the Sonos Arc Ultra, but I think it's going to be exceptionally close. </p><p>Features like Bose's custom tuning and SpeechClarity are genuinely helpful, and I think music sounds fantastic for a soundbar. There are some minor issues like the small soundstage and lack of an HDMI passthrough port that are troubling, however.</p><p>Although it's a bit pricey, the Lifestyle Ultra is a darn good entry in Bose's storied soundbar lineup, and it's one I'm excited to fully test over the next week or two. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I’ve been testing the Bose Lifestyle Ultra speaker for a week — Sonos has some real competition ]]></title>
                                                                                                <dc:content><![CDATA[ <p>What took so long? It’s pretty remarkable that, for all its other products, it has taken Bose this long to come out with an in-home smart speaker. But the Bose Lifestyle Ultra is finally here, and it looks like the company has not only taken lessons from its own rich history of designing audio products, but also from what’s worked — and what hasn’t — from its competitors. </p><p>Apart from delivering excellent audio, this is the first smart speaker not made by Amazon that supports Alexa+; it supports AirPlay, Google Cast, and Spotify Connect, and you can even plug it directly into a turntable. </p><p>But, having used it for a week, I have a few reservations. So how does the Bose Lifestyle Ultra stack up against the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a>? You’ll have to read the rest of my review to find out.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-price-and-availability"><span>Bose Lifestyle Ultra review: Price and availability</span></h3><p>The Bose LifeStyle Ultra Speaker went on sale May 5 2026 at <a href="https://go.redirectingat.com/?id=92X1584493&xcust=tomsguide_us_1498731132257155470&xs=1&url=https%3A%2F%2Fbose.com%2Fp%2Fspeakers%2Fbose-lifestyle-ultra-speaker%2FLSULT-SPEAKERWIRELESS.html&sref=https%3A%2F%2Fwww.tomsguide.com">Bose.com</a>, and is more widely available at other retailers. The speaker comes in three colors: the black and white models cost $299, while a limited-edition Driftwood Sand model costs $349.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-design"><span>Bose Lifestyle Ultra review: Design</span></h3><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/r5gG9SbpUv2e9NBnuaXE7b.jpg" alt="Bose Lifestyle Ultra Speaker next to turntable" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/NnwDjnQ6xPK5VLL9LxL8m3.jpg" alt="Bose Lifestyle Ultra Speaker top front" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/GTKetYSsM66ddafB9VQZCk.jpg" alt="Bose Lifestyle Ultra Speaker top showing controls" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Fr2oroLqEdw7LixJQe3LR.jpg" alt="Bose Lifestyle Ultra Speaker side view" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/oCrCx4qxtCjKbTzvjBPMcV.jpg" alt="Bose Lifestyle Ultra Speaker lower front " /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/mjmdrPXhfZufiTmZyT3T2d.jpg" alt="Bose Lifestyle Ultra Speaker rear" /><figcaption><small role="credit">Future</small></figcaption></figure></figure><div  class="fancy-box"><div class="fancy_box-title">Bose Lifestyle Ultra specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price</strong>: $299 ($349 for Sandstone)<br><strong>Speakers</strong>: One 3-inch driver, 1-inch tweeter, 1.5-inch upward-firing twiddler<br><strong>Supported streaming</strong>: Google Cast, Apple AirPlay, Spotify Connect<br><strong>Connectivity</strong>: Wi-Fi 6, Bluetooth 5.3, 3.5mm<br><strong>Size</strong>: 7.3 x 6.6 x 4.8 inches</p></div></div><p>Stylistically, the Bose speaker falls between the all-plastic <a href="https://www.tomsguide.com/reviews/sonos-era-100">Sonos Era 100</a> and the completely cloth-covered <a href="https://www.tomsguide.com/reviews/apple-homepod-2nd-generation">Apple HomePod 2</a>. The body of Bose’s speaker is made primarily of plastic, but the front and sides are wrapped in an acoustic cloth, which softens the overall look. </p><p>The top of the device has a speaker grille towards the front, and a circular indentation towards the rear with volume controls. In between are additional touch-sensitive controls for Bluetooth and muting the speaker’s microphone. With all the little icons, it feels a bit busy, but that’s just me.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PhFe4DhXLdwD5y3meXnWkd" name="Bose Lifestyle Ultra Speaker" alt="Bose Lifestyle Ultra Speaker - all thee colors" src="https://cdn.mos.cms.futurecdn.net/PhFe4DhXLdwD5y3meXnWkd.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I saw all three color options in person, and the limited-edition Driftwood Sand model definitely stands out, especially with its white oak base. If you have wood decor, it might be worth springing the cash.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-audio-quality"><span>Bose Lifestyle Ultra review: Audio quality</span></h3><p>As you might expect from a Bose product, the sound quality from the Lifestyle Ultra speaker is superb. To test its performance, I streamed a number of tracks through Spotify. </p><p>One thing I noticed in my hands-on — and confirmed in my home — is that the Bose speaker has much brighter mids and highs compared to the Sonos Era 100. As a result, female vocals were especially more pronounced and defined. I could really hear the personality of Carole King singing “I feel the Earth Move,” Dua Lipa in “Dance the Night,” and Kristen Chenoweth belting out “Popular.”</p><p>In fact, the mids were forward sometimes to the point of them sounding a bit too harsh. It was especially evident when I cranked “Tom Sawyer,” Geddy Lee’s falsetto cracked a little bit on the higher lines. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7728px;"><p class="vanilla-image-block" style="padding-top:56.21%;"><img id="NnwDjnQ6xPK5VLL9LxL8m3" name="Bose Lifestyle Ultra Speaker-4" alt="Bose Lifestyle Ultra Speaker top front" src="https://cdn.mos.cms.futurecdn.net/NnwDjnQ6xPK5VLL9LxL8m3.jpg" mos="" align="middle" fullscreen="" width="7728" height="4344" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Lower tones also came through loud and clear; on that same track, the bass line and drums were so crisp that I could hear every bang and thump cleanly. The thumping bass line on the Beastie Boys’ “Paul Revere” also rang through like the silversmith running through Massachusetts warning of the British.</p><p>When played on the Sonos Era 100, those same vocals weren’t as present — things felt a bit rounder and pulled back — but the soundstage also was a bit wider, if not as tall as the Bose. </p><p>Ultimately, choosing between the Bose and the Sonos really comes down to a matter of preference. Both offer superior audio quality, and it’s hard to pick a winner based on sound alone.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7588px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bCWzpbeVYJarnb4okMd5WD" name="Bose Lifestyle Ultra Speaker-9" alt="Bose Lifestyle Ultra Speaker with Sonos Era 100 and Apple HomePod 2" src="https://cdn.mos.cms.futurecdn.net/bCWzpbeVYJarnb4okMd5WD.jpg" mos="" align="middle" fullscreen="" width="7588" height="4268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The upward-firing speaker also added a bit more height to music, which, in the absence of Dolby Atmos support, is a nice little extra. Still, both the HomePod 2 and Amazon Echo Studio support spatial audio, so Atmos’ absence is felt.</p><p>Unlike some of the other smart speakers I’ve tested — most notably the Sonos Era 100, HomePod 2, and even the <a href="https://www.tomsguide.com/audio/smart-speakers/amazon-echo-studio-2025-review">Amazon Echo Studio</a> — the Bose Lifestyle Ultra speaker does not support Bose’s room-tuning feature, CustomTune. </p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-app-and-home-theater-capabilities"><span>Bose Lifestyle Ultra review: App and home theater capabilities </span></h3><p>Maybe Bose learned from Sonos’ lesson, because its app is pretty minimalistic — but in a good way. You really only need it to set up the speaker, connect it to Wi-Fi, Alexa+, and tweak EQ settings. It has a 3-band EQ (treble, mid, and bass) plus a fourth for height. Overall, it has one of the clearest guides to walk you through the setup process that I’ve ever seen.</p><p>After that, there’s little reason why you’d need to use the app, as Bose has designed it to work within the music app of your choice. It’s compatible with Google Cast, Apple Airplay, and Spotify Connect; when I opened Spotify, the Bose speaker was listed there as a music source. </p><p>If all else fails, you can also simply connect to the speaker via Bluetooth, or just plug in a 3.5mm cord in the back. At a time when physical audio connections seem to be disappearing, it’s nice to see Bose keeping this legacy connector around. </p><p>You can also pair two Bose Lifestyle Ultra speakers for stereo sound, group them in different rooms of your house, or add them as part of a home theater surround system. One bummer is that, while the Lifestyle Ultra Speakers can be paired with a Bose Lifestyle Ultra soundbar, they’re not backwards-compatible with other Bose equipment. So, if you already have a Bose soundbar, you won’t be able to use these as surrounds.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-smart-home-features"><span>Bose Lifestyle Ultra review: Smart home features</span></h3><p>The Lifestyle Ultra is the first third-party smart speaker that’s compatible with <a href="https://www.tomsguide.com/ai/ive-been-using-alexa-for-3-months-here-are-my-5-favorite-features-and-one-im-not-a-fan-of">Alexa+</a>, Amazon’s AI-improved voice assistant. However, unlike the HomePod and Echo Studio, the Bose Lifestyle Ultra lacks presence, temperature, or any other sensor of that ilk which would allow you to create automated routines</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-bottom-line"><span>Bose Lifestyle Ultra review: Bottom line</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7193px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Fr2oroLqEdw7LixJQe3LR" name="Bose Lifestyle Ultra Speaker-7" alt="Bose Lifestyle Ultra Speaker side view" src="https://cdn.mos.cms.futurecdn.net/Fr2oroLqEdw7LixJQe3LR.jpg" mos="" align="middle" fullscreen="" width="7193" height="4046" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Bose Lifestyle Ultra is a very capable and well-designed smart speaker. Its audio quality is superb, it’s easy to set up, it works seamlessly with all the major music streaming services and it’s the first third-party smart speaker that can access Alexa+.</p><p>All that being said, it’s sandwiched between some well-established competition. At $299, it’s the same price as the Amazon Echo Studio and Apple HomePod 2, but those models both support spatial audio and have extra sensors to take fuller advantage of their smart home capabilities. </p><p>Bose’s other chief competitor in this market, the <a href="https://www.tomsguide.com/reviews/sonos-era-100">Sonos Era 100</a>, has now been out for three years, costs $80 less, and offers similar audio quality. (Sonos also sells the Alexa-free <a href="https://www.tomsguide.com/audio/smart-speakers/i-found-the-perfect-use-for-sonos-new-era-100-sl-speakers-and-i-couldnt-be-happier">Sonos Era 100 SL</a> for $189, so I suspect that not as many use voice controls as the company originally thought.)</p><p>In addition, all three rivals offer some sort of room-tuning capability, while the Bose does not. </p><p>On the basis of its performance as a speaker to play music from your turntable or streaming service of choice, the Bose Lifestyle Ultra will not disappoint. However, it falls a little short when it comes to the little extras you get with its rivals. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/audio/smart-speakers/bose-lifestyle-ultra-speaker-review</link>
                                                                            <description>
                            <![CDATA[ I've been testing the Bose Lifestyle Ultra Speaker for a week and it sounds amazing, but faces some real competition from established players ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">dFn6TzGg5gDkYsYw3FcW4j</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/pmLMPkvGNnozJS9H7RsCTR-1280-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 15 May 2026 12:00:00 +0000</pubDate>                                                                                                                                <updated>Fri, 15 May 2026 13:33:17 +0000</updated>
                                                                                                                                            <category><![CDATA[Smart Speakers]]></category>
                                                    <category><![CDATA[Audio]]></category>
                                                    <category><![CDATA[Speakers]]></category>
                                                                                                <author><![CDATA[ mike.prospero@futurenet.com (Mike Prospero) ]]></author>                    <dc:creator><![CDATA[ Mike Prospero ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/6ZM8mX4UwccqDJTh9gLPqV.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Michael A. Prospero is the U.S. Editor-in-Chief for Tom’s Guide. He oversees all evergreen content and oversees the Homes, Smart Home, and Fitness/Wearables categories for the site. In his spare time, he also tests out the latest drones, electric scooters, and smart home gadgets, such as video doorbells. Before his tenure at Tom&#039;s Guide, he was the Reviews Editor for Laptop Magazine, a reporter at Fast Company, the Times of Trenton, and, many eons back, an intern at George magazine. He received his undergraduate degree from Boston College, where he worked on the campus newspaper The Heights, and then attended the Columbia University school of Journalism. When he’s not testing out the latest running watch, electric scooter, or skiing or training for a marathon, he’s probably using the latest sous vide machine, smoker, or pizza oven, to the delight — or chagrin — of his family.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/jpeg" url="https://cdn.mos.cms.futurecdn.net/pmLMPkvGNnozJS9H7RsCTR-1280-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Bose Lifestyle Ultra Speaker]]></media:description>                                                            <media:text><![CDATA[Bose Lifestyle Ultra Speaker]]></media:text>
                                <media:title type="plain"><![CDATA[Bose Lifestyle Ultra Speaker]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/pmLMPkvGNnozJS9H7RsCTR-1280-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>What took so long? It’s pretty remarkable that, for all its other products, it has taken Bose this long to come out with an in-home smart speaker. But the Bose Lifestyle Ultra is finally here, and it looks like the company has not only taken lessons from its own rich history of designing audio products, but also from what’s worked — and what hasn’t — from its competitors. </p><p>Apart from delivering excellent audio, this is the first smart speaker not made by Amazon that supports Alexa+; it supports AirPlay, Google Cast, and Spotify Connect, and you can even plug it directly into a turntable. </p><p>But, having used it for a week, I have a few reservations. So how does the Bose Lifestyle Ultra stack up against the <a href="https://www.tomsguide.com/us/best-smart-speakers,review-4480.html">best smart speakers</a>? You’ll have to read the rest of my review to find out.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-price-and-availability"><span>Bose Lifestyle Ultra review: Price and availability</span></h3><p>The Bose LifeStyle Ultra Speaker went on sale May 5 2026 at <a href="https://go.redirectingat.com/?id=92X1584493&xcust=tomsguide_us_1498731132257155470&xs=1&url=https%3A%2F%2Fbose.com%2Fp%2Fspeakers%2Fbose-lifestyle-ultra-speaker%2FLSULT-SPEAKERWIRELESS.html&sref=https%3A%2F%2Fwww.tomsguide.com">Bose.com</a>, and is more widely available at other retailers. The speaker comes in three colors: the black and white models cost $299, while a limited-edition Driftwood Sand model costs $349.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-design"><span>Bose Lifestyle Ultra review: Design</span></h3><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/r5gG9SbpUv2e9NBnuaXE7b.jpg" alt="Bose Lifestyle Ultra Speaker next to turntable" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/NnwDjnQ6xPK5VLL9LxL8m3.jpg" alt="Bose Lifestyle Ultra Speaker top front" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/GTKetYSsM66ddafB9VQZCk.jpg" alt="Bose Lifestyle Ultra Speaker top showing controls" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Fr2oroLqEdw7LixJQe3LR.jpg" alt="Bose Lifestyle Ultra Speaker side view" /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/oCrCx4qxtCjKbTzvjBPMcV.jpg" alt="Bose Lifestyle Ultra Speaker lower front " /><figcaption><small role="credit">Future</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/mjmdrPXhfZufiTmZyT3T2d.jpg" alt="Bose Lifestyle Ultra Speaker rear" /><figcaption><small role="credit">Future</small></figcaption></figure></figure><div  class="fancy-box"><div class="fancy_box-title">Bose Lifestyle Ultra specs</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Price</strong>: $299 ($349 for Sandstone)<br><strong>Speakers</strong>: One 3-inch driver, 1-inch tweeter, 1.5-inch upward-firing twiddler<br><strong>Supported streaming</strong>: Google Cast, Apple AirPlay, Spotify Connect<br><strong>Connectivity</strong>: Wi-Fi 6, Bluetooth 5.3, 3.5mm<br><strong>Size</strong>: 7.3 x 6.6 x 4.8 inches</p></div></div><p>Stylistically, the Bose speaker falls between the all-plastic <a href="https://www.tomsguide.com/reviews/sonos-era-100">Sonos Era 100</a> and the completely cloth-covered <a href="https://www.tomsguide.com/reviews/apple-homepod-2nd-generation">Apple HomePod 2</a>. The body of Bose’s speaker is made primarily of plastic, but the front and sides are wrapped in an acoustic cloth, which softens the overall look. </p><p>The top of the device has a speaker grille towards the front, and a circular indentation towards the rear with volume controls. In between are additional touch-sensitive controls for Bluetooth and muting the speaker’s microphone. With all the little icons, it feels a bit busy, but that’s just me.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PhFe4DhXLdwD5y3meXnWkd" name="Bose Lifestyle Ultra Speaker" alt="Bose Lifestyle Ultra Speaker - all thee colors" src="https://cdn.mos.cms.futurecdn.net/PhFe4DhXLdwD5y3meXnWkd.jpg" mos="" align="middle" fullscreen="" width="2000" height="1125" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>I saw all three color options in person, and the limited-edition Driftwood Sand model definitely stands out, especially with its white oak base. If you have wood decor, it might be worth springing the cash.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-audio-quality"><span>Bose Lifestyle Ultra review: Audio quality</span></h3><p>As you might expect from a Bose product, the sound quality from the Lifestyle Ultra speaker is superb. To test its performance, I streamed a number of tracks through Spotify. </p><p>One thing I noticed in my hands-on — and confirmed in my home — is that the Bose speaker has much brighter mids and highs compared to the Sonos Era 100. As a result, female vocals were especially more pronounced and defined. I could really hear the personality of Carole King singing “I feel the Earth Move,” Dua Lipa in “Dance the Night,” and Kristen Chenoweth belting out “Popular.”</p><p>In fact, the mids were forward sometimes to the point of them sounding a bit too harsh. It was especially evident when I cranked “Tom Sawyer,” Geddy Lee’s falsetto cracked a little bit on the higher lines. </p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7728px;"><p class="vanilla-image-block" style="padding-top:56.21%;"><img id="NnwDjnQ6xPK5VLL9LxL8m3" name="Bose Lifestyle Ultra Speaker-4" alt="Bose Lifestyle Ultra Speaker top front" src="https://cdn.mos.cms.futurecdn.net/NnwDjnQ6xPK5VLL9LxL8m3.jpg" mos="" align="middle" fullscreen="" width="7728" height="4344" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>Lower tones also came through loud and clear; on that same track, the bass line and drums were so crisp that I could hear every bang and thump cleanly. The thumping bass line on the Beastie Boys’ “Paul Revere” also rang through like the silversmith running through Massachusetts warning of the British.</p><p>When played on the Sonos Era 100, those same vocals weren’t as present — things felt a bit rounder and pulled back — but the soundstage also was a bit wider, if not as tall as the Bose. </p><p>Ultimately, choosing between the Bose and the Sonos really comes down to a matter of preference. Both offer superior audio quality, and it’s hard to pick a winner based on sound alone.</p><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7588px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bCWzpbeVYJarnb4okMd5WD" name="Bose Lifestyle Ultra Speaker-9" alt="Bose Lifestyle Ultra Speaker with Sonos Era 100 and Apple HomePod 2" src="https://cdn.mos.cms.futurecdn.net/bCWzpbeVYJarnb4okMd5WD.jpg" mos="" align="middle" fullscreen="" width="7588" height="4268" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The upward-firing speaker also added a bit more height to music, which, in the absence of Dolby Atmos support, is a nice little extra. Still, both the HomePod 2 and Amazon Echo Studio support spatial audio, so Atmos’ absence is felt.</p><p>Unlike some of the other smart speakers I’ve tested — most notably the Sonos Era 100, HomePod 2, and even the <a href="https://www.tomsguide.com/audio/smart-speakers/amazon-echo-studio-2025-review">Amazon Echo Studio</a> — the Bose Lifestyle Ultra speaker does not support Bose’s room-tuning feature, CustomTune. </p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-app-and-home-theater-capabilities"><span>Bose Lifestyle Ultra review: App and home theater capabilities </span></h3><p>Maybe Bose learned from Sonos’ lesson, because its app is pretty minimalistic — but in a good way. You really only need it to set up the speaker, connect it to Wi-Fi, Alexa+, and tweak EQ settings. It has a 3-band EQ (treble, mid, and bass) plus a fourth for height. Overall, it has one of the clearest guides to walk you through the setup process that I’ve ever seen.</p><p>After that, there’s little reason why you’d need to use the app, as Bose has designed it to work within the music app of your choice. It’s compatible with Google Cast, Apple Airplay, and Spotify Connect; when I opened Spotify, the Bose speaker was listed there as a music source. </p><p>If all else fails, you can also simply connect to the speaker via Bluetooth, or just plug in a 3.5mm cord in the back. At a time when physical audio connections seem to be disappearing, it’s nice to see Bose keeping this legacy connector around. </p><p>You can also pair two Bose Lifestyle Ultra speakers for stereo sound, group them in different rooms of your house, or add them as part of a home theater surround system. One bummer is that, while the Lifestyle Ultra Speakers can be paired with a Bose Lifestyle Ultra soundbar, they’re not backwards-compatible with other Bose equipment. So, if you already have a Bose soundbar, you won’t be able to use these as surrounds.</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-smart-home-features"><span>Bose Lifestyle Ultra review: Smart home features</span></h3><p>The Lifestyle Ultra is the first third-party smart speaker that’s compatible with <a href="https://www.tomsguide.com/ai/ive-been-using-alexa-for-3-months-here-are-my-5-favorite-features-and-one-im-not-a-fan-of">Alexa+</a>, Amazon’s AI-improved voice assistant. However, unlike the HomePod and Echo Studio, the Bose Lifestyle Ultra lacks presence, temperature, or any other sensor of that ilk which would allow you to create automated routines</p><h3 class="article-body__section" id="section-bose-lifestyle-ultra-review-bottom-line"><span>Bose Lifestyle Ultra review: Bottom line</span></h3><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7193px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Fr2oroLqEdw7LixJQe3LR" name="Bose Lifestyle Ultra Speaker-7" alt="Bose Lifestyle Ultra Speaker side view" src="https://cdn.mos.cms.futurecdn.net/Fr2oroLqEdw7LixJQe3LR.jpg" mos="" align="middle" fullscreen="" width="7193" height="4046" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Bose Lifestyle Ultra is a very capable and well-designed smart speaker. Its audio quality is superb, it’s easy to set up, it works seamlessly with all the major music streaming services and it’s the first third-party smart speaker that can access Alexa+.</p><p>All that being said, it’s sandwiched between some well-established competition. At $299, it’s the same price as the Amazon Echo Studio and Apple HomePod 2, but those models both support spatial audio and have extra sensors to take fuller advantage of their smart home capabilities. </p><p>Bose’s other chief competitor in this market, the <a href="https://www.tomsguide.com/reviews/sonos-era-100">Sonos Era 100</a>, has now been out for three years, costs $80 less, and offers similar audio quality. (Sonos also sells the Alexa-free <a href="https://www.tomsguide.com/audio/smart-speakers/i-found-the-perfect-use-for-sonos-new-era-100-sl-speakers-and-i-couldnt-be-happier">Sonos Era 100 SL</a> for $189, so I suspect that not as many use voice controls as the company originally thought.)</p><p>In addition, all three rivals offer some sort of room-tuning capability, while the Bose does not. </p><p>On the basis of its performance as a speaker to play music from your turntable or streaming service of choice, the Bose Lifestyle Ultra will not disappoint. However, it falls a little short when it comes to the little extras you get with its rivals. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
            </channel>
</rss>