<?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/peripherals" rel="self" type="application/rss+xml" />
                            <title><![CDATA[ Latest from Tom's Guide in Peripherals ]]></title>
                <link>https://www.tomsguide.com/computing/peripherals</link>
        <description><![CDATA[ All the latest peripherals content from the Tom's Guide team ]]></description>
                                    <lastBuildDate>Fri, 02 Oct 2026 10:40:58 +0000</lastBuildDate>
                            <language>en</language>
                                <item>
                                                            <title><![CDATA[ The Keychron M2 8K is a powerful budget gaming mouse — as long as you don’t mind seeing shiny fingerprints ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Keychron knows how to make a good mouse that packs a punch without breaking the bank — and the Keychron M2 8K is one such rodent. Featuring 8,000Hz polling and adjustable DPI up to 30,000, the M2 8K is a gaming powerhouse. It features Angle Snapping, too, which gives you a competitive advantage in FPS titles. Adding to its appeal is up to 140 hours of runtime, and extensive web-based customization.</p><p>But the M2 8K isn’t perfect. Its scroll wheel is stiff and feels rigid, which has a negative impact on your gaming experience. The body feels quite slippery, and it’s also a fingerprint magnet. The sub-$100 mouse market is overcrowded already, and even the tiniest flaw feels big when you consider the competition.</p><p>So, is it still the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mouse</a> for you? Read my full Keychron M2 8K review to find out.</p><h2 class="article-body__section" id="section-keychron-m2-8k-review-specs"><span>Keychron M2 8K review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.keychron.com/products/keychron-m2-wireless-mouse?variant=42400900612185" target="_blank" rel="nofollow">$69</a> / <a href="https://www.amazon.co.uk/Keychron-Wireless-Bluetooth-Lightweight-Programmable-Black/dp/B0FL71CPLT" target="_blank" rel="nofollow">£54</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>30,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>6</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.78 x 2.63 x 1.6 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.94oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 2.4GHz dongle, Bluetooth 5.3</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>600mAh rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>140 hours (1,000Hz) | 40 hours (8,000Hz)</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-keychron-m2-8k-review-cheat-sheet"><span>Keychron M2 8K review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A wireless gaming mouse with 30,000 DPI and 8,000Hz polling</li><li><strong>Who is it for? </strong>Gamers on a budget</li><li><strong>How much does it cost? </strong>The Keychron M2 8K is available for just <a href="https://www.keychron.com/products/keychron-m2-wireless-mouse?variant=42400900612185" target="_blank" rel="nofollow">$69</a> / <a href="https://www.amazon.co.uk/Keychron-Wireless-Bluetooth-Lightweight-Programmable-Black/dp/B0FL71CPLT" target="_blank" rel="nofollow">£54</a></li><li><strong>What do we like?</strong> The superb gaming performance, long battery life, user-friendly software, and budget-friendly price</li><li><strong>What don’t we like? </strong>The slippery and fingerprint-prone design, and stiff scroll wheel</li></ul><h2 class="article-body__section" id="section-keychron-m2-8k-review-the-ups"><span>Keychron M2 8K review: The ups</span></h2><p>From its 30,000 DPI and 8,000Hz polling rate, to its long battery life and web-based software, the Keychron M2 8K is a solid budget-friendly gaming mouse.</p><h2 id="superb-gaming-performance">Superb gaming 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="FXaePB2QFU3q2jX5gq9cEK" name="Keychron_M2_.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/FXaePB2QFU3q2jX5gq9cEK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>The Keychron M2 8K’s biggest selling point is, of course, its gaming performance. Even though it weighs just 1.94oz, the M2 8K packs a lot of tech. It’s got an 8,000Hz polling rate and a maximum DPI of 30,000 — both of which can be adjusted using the dedicated buttons at the bottom of the mouse.</p><p>8KHz polling in competition mice is fairly commonplace now, as is such high DPI, like the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/steelseries-rival-pro-sensei-pro-review">SteelSeries Rival Pro</a> ($179) with 42,000, the <a href="https://www.tomsguide.com/computing/mice/logitech-g-pro-x2-superstrike-review">Logitech G Pro X2 Superstrike</a> ($179) with 44,000, the <a href="https://www.tomsguide.com/computing/mice/razer-viper-v4-pro-review">Razer Viper V4 Pro</a> ($159) with 50,000, and the <a href="https://www.tomsguide.com/computing/peripherals/fnatic-x-lamzu-maya-8k-maya-x-8k-mouse-review">Fnatic x Lamzu Maya 8K</a> ($119) with 30,000. It’s nice to see the M2 8K go toe-to-toe with these mice at a much lower price.</p><p>First of all, let’s talk about 8,000Hz polling. Regardless of the game I played, from <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> and Counter-Strike 2 to <a href="https://www.tomsguide.com/opinion/lies-of-p-is-my-biggest-gaming-surprise-of-the-year-and-the-bloodborne-sequel-we-deserve">Lies of P</a>, I didn’t experience or notice any latency issues. All of my commands were registered smoothly and instantaneously, and I appreciated the click buttons’ tactile feedback as well. You can always drop the polling rate down to 1,000Hz when you aren’t gaming, as 8KHz can be a drain on system resources (and the mouse’s battery life).</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="eYhFkkNxDoFN7PwCid6F9K" name="Keychron_M2_ 5.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/eYhFkkNxDoFN7PwCid6F9K-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>A high and adjustable DPI of 30,000 proves to be a big advantage across genres, as you can slow down your movements or speed them up, depending on the in-game scenario. For instance, when I wanted precise, smooth and controlled crosshair placement for landing headshots in Counter-Strike 2, I opted for a low DPI like 800.</p><p>This gave me a lot of control over my movement, and when I was trying to execute a headshot in the distance, my shaky hand movements didn’t throw off my aim. It also helps that the M2 8K features <a href="https://www.tomsguide.com/computing/what-is-angle-snapping">Angle Snapping</a> (enabled via the companion app) that smooths out lateral mouse movements to ensure a straight line, helping you keep your cursor at head-level at all times. It ensures that your hand’s natural and minute movements don’t impact your cursor’s movement, thereby giving you an advantage in games like Counter-Strike 2 and Valorant.</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="ucFi4fo53Wc3ivwqxpFEdJ" name="Keychron_M2_ 6.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/ucFi4fo53Wc3ivwqxpFEdJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>Setting the M2 8K to its maximum DPI of 30,000 is probably overkill for most people — I know it is for me — but I guess it’s good to have in case you need it. I opted for a high DPI of 10,000 when I wanted to be super quick in Cyberpunk 2077 and Lies of P. When trying to stealth my way through a base in Cyberpunk 2077, or being surrounded by slow-moving puppet enemies in Lies of P, I was able to use the mouse’s high DPI to quickly move my camera and scan my surroundings. It also enabled me to change directions really quickly while in close-quarter combat.</p><h2 id="extensive-web-based-customization">Extensive web-based customization</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/bws7sP23s4iorMY6y8ALuT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/hjM4KqVdYrQKYaaFn2V8xT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Ly2uhn23rTeyZ8VG9KFMqT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/d3cVCuA2it2ZhuTFUGmDnT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure></figure><p>The Keychron M2 8K can be extensively customized via the web-based <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher </a>app. Keychron Launcher is one of my favorite apps, as it sports a very clean interface, doesn’t feature intrusive pop-ups (unlike my archnemesis Asus Armoury Crate), and best of all, it can be used without having to install any third-party bloatware on to your machine.</p><p>Once you’ve plugged in your M2 8K and opened Keychron Launcher in Google Chrome (or other browser), you’re able to assign different DPI values and polling rates to their dedicated buttons, toggle smart features like Angle Snapping, change scroll direction, and update the mouse’s firmware. Keychron Launcher is super easy to use, and each setting is accompanied by a short explanation so that you understand what each function does.</p><h2 id="up-to-140-hours-of-battery-life">Up to 140 hours of battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RGwTTxgzKroSTndd4fJF4K" name="Keychron_M2_ 3.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/RGwTTxgzKroSTndd4fJF4K-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>Keychron has spared no expense in ensuring that the M2 8K lasts as long as possible — and for a budget-friendly mouse, it boasts exceptional battery life. The M2 8K is equipped with the same 600mAh rechargeable battery that’s also found in the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini 8K</a> ($69). When polling at 1,000Hz, the M2 8K is rated for up to 140 hours. 8,000Hz polling is naturally detrimental to any device’s runtime, and the M2 8K isn’t an exception to that. Its battery life drops to 40 hours when polling at 8,000Hz — but that’s still excellent.</p><p>In comparison, both the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69) and the pricier Fnatic x Lamzu Maya 8K are rated for 80 hours each at 1,000Hz. The M2 8K is bested by the likes of the Razer Viper V4 Pro with its 45-hour runtime when polling at 8,000Hz, and the SteelSeries Rival Pro with its zero-downtime battery — but they cost <em>a lot </em>more than the M2 8K.</p><h2 id="budget-friendly">Budget-friendly</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3dVbd9Rq6X9vJbNwXHYrxJ" name="Keychron_M2_ 4.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/3dVbd9Rq6X9vJbNwXHYrxJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>On top of everything the Keychron M2 8K offers, it’s the mouse’s price that really sweetens the deal. Priced at just <a href="https://www.keychron.com/products/keychron-m2-wireless-mouse?variant=42400900612185" target="_blank" rel="nofollow">$69</a> / <a href="https://www.amazon.co.uk/Keychron-Wireless-Bluetooth-Lightweight-Programmable-Black/dp/B0FL71CPLT" target="_blank" rel="nofollow">£54</a>, it’s one of the cheapest mice you can buy today. There’s no dearth of competition in that price range, and the M2 8K goes up against heavy hitters, like the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m7-review">Keychron M7</a> ($49), the Keychron M3 Mini 8K ($69), and the <a href="https://www.tomsguide.com/computing/peripherals/nzxt-lift-elite-wireless-gaming-mouse-review">NZXT Lift Elite Wireless</a> ($79). The M2 8K leaves its mark as one of the best, though, and is great for anyone who wants a simple-looking but powerful gaming mouse.</p><h2 class="article-body__section" id="section-keychron-m2-8k-review-the-downs"><span>Keychron M2 8K review: The downs</span></h2><p>Unfortunately, the Keychron M2 8K has a few flaws. Its scroll wheel is very stiff, the mouse itself proved slippery in testing, and it’s a fingerprint magnet.</p><h2 id="stiff-scroll-wheel">Stiff scroll wheel</h2><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="RGwTTxgzKroSTndd4fJF4K" name="Keychron_M2_ 3.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:160,l:504,cw:960,ch:540,q:80/RGwTTxgzKroSTndd4fJF4K.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>One of the things that’s annoyed me most about using the Keychron M2 8K is its stiff scroll wheel. It feels like it has high resistance, and it feels heavy, tight and hard to rotate. In addition to that, the scroll click also feels heavy. While that’s great for avoiding accidental clicks while, say, scrolling through a spreadsheet, it can cause finger fatigue and impact reaction times in video games.</p><p>The M2 8K’s scroll wheel certainly had an impact on my experience when playing Cyberpunk 2077. When in the heat of combat, I wasn’t able to swap between my quick-access weapons as quickly as I’d have liked, because the scroll wheel felt heavy. I haven’t experienced this issue with other Keychron mice, like the M6 8K or the M3 Mini 8K. Maybe the M2 8K’s scroll wheel will loosen over time — but I wish it felt smooth straight out of the box.</p><h2 id="slippery-body">Slippery body</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8nszXabMPRwvq9PTS5JQvJ" name="Keychron_M2_ 2.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/8nszXabMPRwvq9PTS5JQvJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>While I’ve found the Keychron M2 8K mostly comfortable, it proved quite slippery in testing. The uncoated, smooth ABS plastic lacks inherent grip. If your hands tend to sweat a lot, you won’t be pleased with this mouse, as the smooth surface becomes harder to hold during intense gaming sessions.</p><p>This issue can be fixed by sticking grip tape to the M2 8K — but frustratingly, Keychron doesn’t include any in the mouse’s box, so you’ll have to grab some third-party one. If you want a textured mouse that feels grippy, I’d recommend the Keychron M3 Mini V2 8K or the NZXT Lift Elite Wireless. Neither gave me any grip issues during testing. Elite esports mice, like the Razer Viper V4 Pro and the Logitech G Pro X2 Superstrike, feel grippy too, so it’s a problem with the M2 8K, not competition mice as a whole.</p><h2 id="fingerprint-magnet">Fingerprint magnet</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QaFRyUUZdWwXFfRuToDrkJ" name="Keychron_M2" alt="Fingerprints on a Keychron M2 8K" src="https://cdn.mos.cms.futurecdn.net/QaFRyUUZdWwXFfRuToDrkJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" attribution="" endorsement="" class="inline"></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 isn’t a major drawback but for anyone who gets the ick easily may want to consider this before buying the Keychron M2 8K. Thanks to its matte ABS coating, this thing is a fingerprint magnet. I noticed my fingerprints shining on the mouse just after an hour of use. We had the same issue with the SteelSeries Rival and Sensei Pro, so this isn’t isolated to Keychron. But if it bothers you, grab the NZXT Lift Elite Wireless or the <a href="https://www.tomsguide.com/gaming/peripherals/lemokey-g1-review">Lemokey G1</a> ($64) instead.</p><h2 class="article-body__section" id="section-keychron-m2-8k-review-verdict"><span>Keychron M2 8K 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="4ff3xdZhHK4vL4Va5JGmWJ" name="Keychron_M2_ 1.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/4ff3xdZhHK4vL4Va5JGmWJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>The Keychron M2 8K is a competent little rodent. It’s great for FPS and RPGs as it packs 8,000Hz polling and 30,000 DPI, both of which can be adjusted on the fly, thanks to the buttons on the bottom. When polling at 1,000Hz, the mouse can last up to a whopping 140 hours, too. Oh, and customization is easy as pie via web-based software.</p><p>However, the M2 8K has a few flaws. It picks up fingerprints extremely easily and quickly, and the mouse’s body feels quite slippery. The M2 8K’s scroll wheel feels very stiff and rigid, too, which has a detrimental effect on gaming performance. For $69 or just under, you can get a more well-rounded mouse, like the Lemokey G1 or the Keychron M3 Mini V2 8K. Though the M2 8K is good, it’s simply outdone by others.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/mice/keychron-m2-8k-review</link>
                                                                            <description>
                            <![CDATA[ The Keychron M2 8K is a powerful gaming mouse with 30,000 DPI and 8,000Hz polling, and it features extensive customization. But it has some flaws. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">Jph9qEbmYJnqEJzzcmquUh</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/FctjwSo9j6Kj5Fo3Tm5mHK-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 02 Oct 2026 10:40:58 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Mice]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/FctjwSo9j6Kj5Fo3Tm5mHK-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guidw]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Keychron M2 8K wireless gaming mouse]]></media:description>                                                            <media:text><![CDATA[A black Keychron M2 8K wireless gaming mouse]]></media:text>
                                <media:title type="plain"><![CDATA[A black Keychron M2 8K wireless gaming mouse]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/FctjwSo9j6Kj5Fo3Tm5mHK-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Keychron knows how to make a good mouse that packs a punch without breaking the bank — and the Keychron M2 8K is one such rodent. Featuring 8,000Hz polling and adjustable DPI up to 30,000, the M2 8K is a gaming powerhouse. It features Angle Snapping, too, which gives you a competitive advantage in FPS titles. Adding to its appeal is up to 140 hours of runtime, and extensive web-based customization.</p><p>But the M2 8K isn’t perfect. Its scroll wheel is stiff and feels rigid, which has a negative impact on your gaming experience. The body feels quite slippery, and it’s also a fingerprint magnet. The sub-$100 mouse market is overcrowded already, and even the tiniest flaw feels big when you consider the competition.</p><p>So, is it still the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mouse</a> for you? Read my full Keychron M2 8K review to find out.</p><h2 class="article-body__section" id="section-keychron-m2-8k-review-specs"><span>Keychron M2 8K review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.keychron.com/products/keychron-m2-wireless-mouse?variant=42400900612185" target="_blank" rel="nofollow">$69</a> / <a href="https://www.amazon.co.uk/Keychron-Wireless-Bluetooth-Lightweight-Programmable-Black/dp/B0FL71CPLT" target="_blank" rel="nofollow">£54</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>30,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>6</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.78 x 2.63 x 1.6 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.94oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 2.4GHz dongle, Bluetooth 5.3</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>600mAh rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>140 hours (1,000Hz) | 40 hours (8,000Hz)</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-keychron-m2-8k-review-cheat-sheet"><span>Keychron M2 8K review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A wireless gaming mouse with 30,000 DPI and 8,000Hz polling</li><li><strong>Who is it for? </strong>Gamers on a budget</li><li><strong>How much does it cost? </strong>The Keychron M2 8K is available for just <a href="https://www.keychron.com/products/keychron-m2-wireless-mouse?variant=42400900612185" target="_blank" rel="nofollow">$69</a> / <a href="https://www.amazon.co.uk/Keychron-Wireless-Bluetooth-Lightweight-Programmable-Black/dp/B0FL71CPLT" target="_blank" rel="nofollow">£54</a></li><li><strong>What do we like?</strong> The superb gaming performance, long battery life, user-friendly software, and budget-friendly price</li><li><strong>What don’t we like? </strong>The slippery and fingerprint-prone design, and stiff scroll wheel</li></ul><h2 class="article-body__section" id="section-keychron-m2-8k-review-the-ups"><span>Keychron M2 8K review: The ups</span></h2><p>From its 30,000 DPI and 8,000Hz polling rate, to its long battery life and web-based software, the Keychron M2 8K is a solid budget-friendly gaming mouse.</p><h2 id="superb-gaming-performance">Superb gaming 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="FXaePB2QFU3q2jX5gq9cEK" name="Keychron_M2_.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/FXaePB2QFU3q2jX5gq9cEK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>The Keychron M2 8K’s biggest selling point is, of course, its gaming performance. Even though it weighs just 1.94oz, the M2 8K packs a lot of tech. It’s got an 8,000Hz polling rate and a maximum DPI of 30,000 — both of which can be adjusted using the dedicated buttons at the bottom of the mouse.</p><p>8KHz polling in competition mice is fairly commonplace now, as is such high DPI, like the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/steelseries-rival-pro-sensei-pro-review">SteelSeries Rival Pro</a> ($179) with 42,000, the <a href="https://www.tomsguide.com/computing/mice/logitech-g-pro-x2-superstrike-review">Logitech G Pro X2 Superstrike</a> ($179) with 44,000, the <a href="https://www.tomsguide.com/computing/mice/razer-viper-v4-pro-review">Razer Viper V4 Pro</a> ($159) with 50,000, and the <a href="https://www.tomsguide.com/computing/peripherals/fnatic-x-lamzu-maya-8k-maya-x-8k-mouse-review">Fnatic x Lamzu Maya 8K</a> ($119) with 30,000. It’s nice to see the M2 8K go toe-to-toe with these mice at a much lower price.</p><p>First of all, let’s talk about 8,000Hz polling. Regardless of the game I played, from <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> and Counter-Strike 2 to <a href="https://www.tomsguide.com/opinion/lies-of-p-is-my-biggest-gaming-surprise-of-the-year-and-the-bloodborne-sequel-we-deserve">Lies of P</a>, I didn’t experience or notice any latency issues. All of my commands were registered smoothly and instantaneously, and I appreciated the click buttons’ tactile feedback as well. You can always drop the polling rate down to 1,000Hz when you aren’t gaming, as 8KHz can be a drain on system resources (and the mouse’s battery life).</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="eYhFkkNxDoFN7PwCid6F9K" name="Keychron_M2_ 5.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/eYhFkkNxDoFN7PwCid6F9K-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>A high and adjustable DPI of 30,000 proves to be a big advantage across genres, as you can slow down your movements or speed them up, depending on the in-game scenario. For instance, when I wanted precise, smooth and controlled crosshair placement for landing headshots in Counter-Strike 2, I opted for a low DPI like 800.</p><p>This gave me a lot of control over my movement, and when I was trying to execute a headshot in the distance, my shaky hand movements didn’t throw off my aim. It also helps that the M2 8K features <a href="https://www.tomsguide.com/computing/what-is-angle-snapping">Angle Snapping</a> (enabled via the companion app) that smooths out lateral mouse movements to ensure a straight line, helping you keep your cursor at head-level at all times. It ensures that your hand’s natural and minute movements don’t impact your cursor’s movement, thereby giving you an advantage in games like Counter-Strike 2 and Valorant.</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="ucFi4fo53Wc3ivwqxpFEdJ" name="Keychron_M2_ 6.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/ucFi4fo53Wc3ivwqxpFEdJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>Setting the M2 8K to its maximum DPI of 30,000 is probably overkill for most people — I know it is for me — but I guess it’s good to have in case you need it. I opted for a high DPI of 10,000 when I wanted to be super quick in Cyberpunk 2077 and Lies of P. When trying to stealth my way through a base in Cyberpunk 2077, or being surrounded by slow-moving puppet enemies in Lies of P, I was able to use the mouse’s high DPI to quickly move my camera and scan my surroundings. It also enabled me to change directions really quickly while in close-quarter combat.</p><h2 id="extensive-web-based-customization">Extensive web-based customization</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/bws7sP23s4iorMY6y8ALuT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/hjM4KqVdYrQKYaaFn2V8xT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Ly2uhn23rTeyZ8VG9KFMqT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/d3cVCuA2it2ZhuTFUGmDnT-1920-80.jpg" alt="Keychron Launcher screenshot" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure></figure><p>The Keychron M2 8K can be extensively customized via the web-based <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher </a>app. Keychron Launcher is one of my favorite apps, as it sports a very clean interface, doesn’t feature intrusive pop-ups (unlike my archnemesis Asus Armoury Crate), and best of all, it can be used without having to install any third-party bloatware on to your machine.</p><p>Once you’ve plugged in your M2 8K and opened Keychron Launcher in Google Chrome (or other browser), you’re able to assign different DPI values and polling rates to their dedicated buttons, toggle smart features like Angle Snapping, change scroll direction, and update the mouse’s firmware. Keychron Launcher is super easy to use, and each setting is accompanied by a short explanation so that you understand what each function does.</p><h2 id="up-to-140-hours-of-battery-life">Up to 140 hours of battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RGwTTxgzKroSTndd4fJF4K" name="Keychron_M2_ 3.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/RGwTTxgzKroSTndd4fJF4K-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>Keychron has spared no expense in ensuring that the M2 8K lasts as long as possible — and for a budget-friendly mouse, it boasts exceptional battery life. The M2 8K is equipped with the same 600mAh rechargeable battery that’s also found in the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini 8K</a> ($69). When polling at 1,000Hz, the M2 8K is rated for up to 140 hours. 8,000Hz polling is naturally detrimental to any device’s runtime, and the M2 8K isn’t an exception to that. Its battery life drops to 40 hours when polling at 8,000Hz — but that’s still excellent.</p><p>In comparison, both the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69) and the pricier Fnatic x Lamzu Maya 8K are rated for 80 hours each at 1,000Hz. The M2 8K is bested by the likes of the Razer Viper V4 Pro with its 45-hour runtime when polling at 8,000Hz, and the SteelSeries Rival Pro with its zero-downtime battery — but they cost <em>a lot </em>more than the M2 8K.</p><h2 id="budget-friendly">Budget-friendly</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3dVbd9Rq6X9vJbNwXHYrxJ" name="Keychron_M2_ 4.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/3dVbd9Rq6X9vJbNwXHYrxJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>On top of everything the Keychron M2 8K offers, it’s the mouse’s price that really sweetens the deal. Priced at just <a href="https://www.keychron.com/products/keychron-m2-wireless-mouse?variant=42400900612185" target="_blank" rel="nofollow">$69</a> / <a href="https://www.amazon.co.uk/Keychron-Wireless-Bluetooth-Lightweight-Programmable-Black/dp/B0FL71CPLT" target="_blank" rel="nofollow">£54</a>, it’s one of the cheapest mice you can buy today. There’s no dearth of competition in that price range, and the M2 8K goes up against heavy hitters, like the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m7-review">Keychron M7</a> ($49), the Keychron M3 Mini 8K ($69), and the <a href="https://www.tomsguide.com/computing/peripherals/nzxt-lift-elite-wireless-gaming-mouse-review">NZXT Lift Elite Wireless</a> ($79). The M2 8K leaves its mark as one of the best, though, and is great for anyone who wants a simple-looking but powerful gaming mouse.</p><h2 class="article-body__section" id="section-keychron-m2-8k-review-the-downs"><span>Keychron M2 8K review: The downs</span></h2><p>Unfortunately, the Keychron M2 8K has a few flaws. Its scroll wheel is very stiff, the mouse itself proved slippery in testing, and it’s a fingerprint magnet.</p><h2 id="stiff-scroll-wheel">Stiff scroll wheel</h2><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="RGwTTxgzKroSTndd4fJF4K" name="Keychron_M2_ 3.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:160,l:504,cw:960,ch:540,q:80/RGwTTxgzKroSTndd4fJF4K.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>One of the things that’s annoyed me most about using the Keychron M2 8K is its stiff scroll wheel. It feels like it has high resistance, and it feels heavy, tight and hard to rotate. In addition to that, the scroll click also feels heavy. While that’s great for avoiding accidental clicks while, say, scrolling through a spreadsheet, it can cause finger fatigue and impact reaction times in video games.</p><p>The M2 8K’s scroll wheel certainly had an impact on my experience when playing Cyberpunk 2077. When in the heat of combat, I wasn’t able to swap between my quick-access weapons as quickly as I’d have liked, because the scroll wheel felt heavy. I haven’t experienced this issue with other Keychron mice, like the M6 8K or the M3 Mini 8K. Maybe the M2 8K’s scroll wheel will loosen over time — but I wish it felt smooth straight out of the box.</p><h2 id="slippery-body">Slippery body</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8nszXabMPRwvq9PTS5JQvJ" name="Keychron_M2_ 2.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/8nszXabMPRwvq9PTS5JQvJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>While I’ve found the Keychron M2 8K mostly comfortable, it proved quite slippery in testing. The uncoated, smooth ABS plastic lacks inherent grip. If your hands tend to sweat a lot, you won’t be pleased with this mouse, as the smooth surface becomes harder to hold during intense gaming sessions.</p><p>This issue can be fixed by sticking grip tape to the M2 8K — but frustratingly, Keychron doesn’t include any in the mouse’s box, so you’ll have to grab some third-party one. If you want a textured mouse that feels grippy, I’d recommend the Keychron M3 Mini V2 8K or the NZXT Lift Elite Wireless. Neither gave me any grip issues during testing. Elite esports mice, like the Razer Viper V4 Pro and the Logitech G Pro X2 Superstrike, feel grippy too, so it’s a problem with the M2 8K, not competition mice as a whole.</p><h2 id="fingerprint-magnet">Fingerprint magnet</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QaFRyUUZdWwXFfRuToDrkJ" name="Keychron_M2" alt="Fingerprints on a Keychron M2 8K" src="https://cdn.mos.cms.futurecdn.net/QaFRyUUZdWwXFfRuToDrkJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" attribution="" endorsement="" class="inline"></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 isn’t a major drawback but for anyone who gets the ick easily may want to consider this before buying the Keychron M2 8K. Thanks to its matte ABS coating, this thing is a fingerprint magnet. I noticed my fingerprints shining on the mouse just after an hour of use. We had the same issue with the SteelSeries Rival and Sensei Pro, so this isn’t isolated to Keychron. But if it bothers you, grab the NZXT Lift Elite Wireless or the <a href="https://www.tomsguide.com/gaming/peripherals/lemokey-g1-review">Lemokey G1</a> ($64) instead.</p><h2 class="article-body__section" id="section-keychron-m2-8k-review-verdict"><span>Keychron M2 8K 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="4ff3xdZhHK4vL4Va5JGmWJ" name="Keychron_M2_ 1.JPG" alt="A black Keychron M2 8K wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/4ff3xdZhHK4vL4Va5JGmWJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guidw)</span></figcaption></figure><p>The Keychron M2 8K is a competent little rodent. It’s great for FPS and RPGs as it packs 8,000Hz polling and 30,000 DPI, both of which can be adjusted on the fly, thanks to the buttons on the bottom. When polling at 1,000Hz, the mouse can last up to a whopping 140 hours, too. Oh, and customization is easy as pie via web-based software.</p><p>However, the M2 8K has a few flaws. It picks up fingerprints extremely easily and quickly, and the mouse’s body feels quite slippery. The M2 8K’s scroll wheel feels very stiff and rigid, too, which has a detrimental effect on gaming performance. For $69 or just under, you can get a more well-rounded mouse, like the Lemokey G1 or the Keychron M3 Mini V2 8K. Though the M2 8K is good, it’s simply outdone by others.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I tried Microsoft’s new haptic Surface Mouse — and standard mice now feel completely broken ]]></title>
                                                                                                <dc:content><![CDATA[ <p>When Microsoft introduced its new Surface lineup at Snapdragon Summit, attention naturally gravitated toward the silicon and the screens. I mean it did to me — fawning over the new <a href="https://www.tomsguide.com/computing/laptops/microsoft-surface-pro-12-inch-surface-laptop-13-inch-snapdragon-x2-plus-hands-on-review">Surface Pro and Surface Laptop armed with Snapdragon X2 Plus</a>.</p><p>But yet, the quietest peripheral on the table — the redesigned <a href="https://www.microsoft.com/en-us/d/surface-mouse-2nd-edition/8wg5vqp0x4bv" target="_blank" rel="nofollow">$79 Surface Mouse</a> — might be the most significant announcement of them all. </p><p>Beyond its 30% recycled chassis, ambidextrous silhouette and dedicated action button, the standout feature is that the integrated haptic feedback can actually communicate with you as you work.</p><p>Let me explain.</p><h2 id="closing-the-tactile-feedback-loop">Closing the tactile feedback loop</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DAvxPoT3rq67pnYCTxdZTJ" name="Surface Laptop 13-inch and Surface Pro 12-inch" alt="Surface Laptop 13-inch and Surface Pro 12-inch" src="https://cdn.mos.cms.futurecdn.net/DAvxPoT3rq67pnYCTxdZTJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" 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>For decades, the computer mouse has been an entirely one-way transmission line: your hand clicks or drags, and your eyes watch the screen to confirm the result. Trackpads have experimented with haptics to mimic mechanical switches, but standalone desktop mice have largely remained hollow plastic shells with rigid microswitches.</p><p>The Surface Mouse turns that input into a two-way conversation in some small but significant ways:</p><ul><li><strong>Window snapping: </strong>Drag a browser window toward the edge of your screen, and the mouse body delivers a subtle tactile bump when Windows 11 snaps the pane into place. It’s one of a few different haptic responses you’ll feel across the OS, and adds a liveliness to the whole UI.</li><li><strong>Object alignment: </strong>Slide an element across any of the Microsoft Office applications, or in Adobe’s Creative Suite, and I physically felt a micro-tick when the shape locks onto an alignment guide or center axis. This will be massive for me — someone with zero alignment awareness — who always falls out trying to snap objects onto ruler lines.</li><li><strong>Tactile verification: </strong>Instead of guessing whether a click registered in a laggy software pipeline, the haptic actuator provides an instant physical confirmation under your palm. If you’re doing anything particularly heavy, it’s good to know separate of the machine running slow that the click has been acknowledged.</li></ul><h2 id="the-takeaway">The takeaway</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="itXXNePGeosLcJXDt3MRpW" name="Microsoft Surface Mouse" alt="Microsoft Surface Mouse" src="https://cdn.mos.cms.futurecdn.net/itXXNePGeosLcJXDt3MRpW-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" 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>Haptic feedback in a mouse sounds gimmicky on a spec sheet. I’ve seen it before in the <a href="https://www.tomsguide.com/computing/mice/logitech-mx-master-4-review">Logitech MX Master 4</a>, and while it feels good, it doesn’t really do anything.</p><p>But in real-world productivity workflows, the Surface Mouse is transformative. It offloads visual verification to your fingertips, shaving seconds off repetitive alignment tasks. </p><p>That Copilot key may grab the headlines, but the tactile dialogue inside the Surface Mouse is the game changer here.</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/computing/laptops/asus-zenbook-14-2026-review"><strong>I stress-tested the Asus Zenbook 14 for college — it’s a great fit, but Windows on 8GB RAM is a problem</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/gaming/pc-gaming/i-benchmarked-the-real-dlss-5-in-nba-2k27-and-the-future-of-photorealistic-graphics-has-one-big-catch"><strong>I tested DLSS 5 in NBA 2K27 on a mid-range gaming laptop — it's the future of photorealistic graphics, but there's a catch</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/computing/smart-glasses/snaps-ceo-told-me-he-hates-getting-ai-drafted-texts-from-his-wife-so-whats-the-plan-to-fight-brain-fry-with-specs"><strong>Snap’s CEO told me he hates getting AI-drafted texts from his wife — so what’s the plan to fight brain fry with Specs?</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/i-tried-microsofts-new-haptic-surface-mouse-standard-mice-now-feel-completely-broken-to-me</link>
                                                                            <description>
                            <![CDATA[ I tested Microsoft’s new $79 haptic Surface Mouse at Snapdragon Summit. From window snapping to object alignment, here is why standard mice now feel broken. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">Sb6fE3SDYVAKJ555bpgLpN</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/VA4yv2CSdVLifecLQTr6fQ-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 26 Sep 2026 05:15:00 +0000</pubDate>                                                                                                                                <updated>Tue, 29 Sep 2026 00:43:59 +0000</updated>
                                                                                                                                            <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                                    <dc:creator><![CDATA[ Jason England ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/v4fSq5U4uZUEtGY2BwNuJ6-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Jason brings a decade of tech and gaming journalism experience to his role as a Managing Editor of Computing at Tom&#039;s Guide. He has previously written for Laptop Mag, Tom&#039;s Hardware, Kotaku, Stuff and BBC Science Focus. In his spare time, you&#039;ll find Jason looking for good dogs to pet or thinking about eating pizza if he isn&#039;t already.&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/VA4yv2CSdVLifecLQTr6fQ-1920-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Microsoft Surface Mouse]]></media:description>                                                            <media:text><![CDATA[Microsoft Surface Mouse]]></media:text>
                                <media:title type="plain"><![CDATA[Microsoft Surface Mouse]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/VA4yv2CSdVLifecLQTr6fQ-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>When Microsoft introduced its new Surface lineup at Snapdragon Summit, attention naturally gravitated toward the silicon and the screens. I mean it did to me — fawning over the new <a href="https://www.tomsguide.com/computing/laptops/microsoft-surface-pro-12-inch-surface-laptop-13-inch-snapdragon-x2-plus-hands-on-review">Surface Pro and Surface Laptop armed with Snapdragon X2 Plus</a>.</p><p>But yet, the quietest peripheral on the table — the redesigned <a href="https://www.microsoft.com/en-us/d/surface-mouse-2nd-edition/8wg5vqp0x4bv" target="_blank" rel="nofollow">$79 Surface Mouse</a> — might be the most significant announcement of them all. </p><p>Beyond its 30% recycled chassis, ambidextrous silhouette and dedicated action button, the standout feature is that the integrated haptic feedback can actually communicate with you as you work.</p><p>Let me explain.</p><h2 id="closing-the-tactile-feedback-loop">Closing the tactile feedback loop</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DAvxPoT3rq67pnYCTxdZTJ" name="Surface Laptop 13-inch and Surface Pro 12-inch" alt="Surface Laptop 13-inch and Surface Pro 12-inch" src="https://cdn.mos.cms.futurecdn.net/DAvxPoT3rq67pnYCTxdZTJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" 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>For decades, the computer mouse has been an entirely one-way transmission line: your hand clicks or drags, and your eyes watch the screen to confirm the result. Trackpads have experimented with haptics to mimic mechanical switches, but standalone desktop mice have largely remained hollow plastic shells with rigid microswitches.</p><p>The Surface Mouse turns that input into a two-way conversation in some small but significant ways:</p><ul><li><strong>Window snapping: </strong>Drag a browser window toward the edge of your screen, and the mouse body delivers a subtle tactile bump when Windows 11 snaps the pane into place. It’s one of a few different haptic responses you’ll feel across the OS, and adds a liveliness to the whole UI.</li><li><strong>Object alignment: </strong>Slide an element across any of the Microsoft Office applications, or in Adobe’s Creative Suite, and I physically felt a micro-tick when the shape locks onto an alignment guide or center axis. This will be massive for me — someone with zero alignment awareness — who always falls out trying to snap objects onto ruler lines.</li><li><strong>Tactile verification: </strong>Instead of guessing whether a click registered in a laggy software pipeline, the haptic actuator provides an instant physical confirmation under your palm. If you’re doing anything particularly heavy, it’s good to know separate of the machine running slow that the click has been acknowledged.</li></ul><h2 id="the-takeaway">The takeaway</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="itXXNePGeosLcJXDt3MRpW" name="Microsoft Surface Mouse" alt="Microsoft Surface Mouse" src="https://cdn.mos.cms.futurecdn.net/itXXNePGeosLcJXDt3MRpW-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" 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>Haptic feedback in a mouse sounds gimmicky on a spec sheet. I’ve seen it before in the <a href="https://www.tomsguide.com/computing/mice/logitech-mx-master-4-review">Logitech MX Master 4</a>, and while it feels good, it doesn’t really do anything.</p><p>But in real-world productivity workflows, the Surface Mouse is transformative. It offloads visual verification to your fingertips, shaving seconds off repetitive alignment tasks. </p><p>That Copilot key may grab the headlines, but the tactile dialogue inside the Surface Mouse is the game changer here.</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/computing/laptops/asus-zenbook-14-2026-review"><strong>I stress-tested the Asus Zenbook 14 for college — it’s a great fit, but Windows on 8GB RAM is a problem</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/gaming/pc-gaming/i-benchmarked-the-real-dlss-5-in-nba-2k27-and-the-future-of-photorealistic-graphics-has-one-big-catch"><strong>I tested DLSS 5 in NBA 2K27 on a mid-range gaming laptop — it's the future of photorealistic graphics, but there's a catch</strong></a><strong></strong></li><li><strong></strong><a href="https://www.tomsguide.com/computing/smart-glasses/snaps-ceo-told-me-he-hates-getting-ai-drafted-texts-from-his-wife-so-whats-the-plan-to-fight-brain-fry-with-specs"><strong>Snap’s CEO told me he hates getting AI-drafted texts from his wife — so what’s the plan to fight brain fry with Specs?</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ This bold, anime-themed MelGeek Centauri60 keyboard just became my daily driver — here’s why ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Having recently spent time with MelGeek’s Made68 Ultra V2 keyboard, I came away declaring my love for its <a href="https://www.tomsguide.com/computing/keyboards/ive-been-using-melgeeks-made68-ultra-v2-for-three-weeks-and-im-in-love-with-its-magnetic-switches-slick-styling-and-clean-sound">magnetic switches, slick styling and clean sound</a>. With those things fresh in my mind, I approached the <a href="https://www.melgeek.com/en-eu/pages/melgeek-x-evangelion-centauri60-asuka-edition" target="_blank" rel="nofollow">Centauri60 Evangelion ‘Asuka Edition’</a> expecting a familiar experience. What I found instead was a keyboard that carries over everything I liked about the Ultra V2 while improving the parts that matter most to me in everyday use.</p><p>The Asuka Edition isn’t exactly subtle — it leans into its Evangelion theme and makes no effort to hide it. You get a ‘dominant’ red case, sharp angled edges, lighting strips on both sides, a hidden engraving of Asuka’s most iconic line, a Prog‑Knife motif, a metal nameplate built into the frame, and a laser‑etched EVA‑02 insignia. It’s unapologetically bold and openly geeky, far more so than the Made68 Ultra V2 I tested earlier, and that personality hits you straight away.</p><p>And yes, the volume knob is still one of those small touches that makes a big impact. On a 60% board, having a tactile media control feels like a luxury, and it’s one I’m thankful for given its absence from the Made 68 Ultra V2. Of course, there’s more to the Centauri60 than its theme —  it’s a keyboard which so resoundingly nails its build, sound, feel, gaming features and typing experience, that it’s now become my daily driver at home — here’s why.</p><h2 id="a-thocking-spectacular-sound-profile">A thocking spectacular sound profile</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hbjiSdJGBPczWwBjHoMfLM" name="9365" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/hbjiSdJGBPczWwBjHoMfLM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 acoustic profile is where the Centauri60 surprised me most. The Ultra V2 I tested sounded super clean, but it used KOM Lite switches which resulted in a brighter, snappier sound signature. The Centauri60, by contrast, comes installed with TTC Flip King magnetic switches, which I instantly preferred for their deeper, more muted tone. </p><p>Another major contributor has to be its CNC aluminium unibody, which weighs a hefty 1.32kg and provides a dense, vibration‑absorbing foundation. Compared to some of the more plastic-centric keyboards I’ve used in the past, this heavy-duty body noticeably suppresses higher‑frequency noise while letting lower tones develop more fully, giving the board a richer, more grounded sound. </p><p>The Centauri60 also uses a five‑layer acoustic dampening system — a combination of Poron foam, PET film, and a leaf‑spring gasket mount — designed to eliminate hollow cavities and absorb unwanted resonance. The result, in my estimation, is a keyboard that minimises harsh clacks for a smoother, more “creamy” signature</p><p>The magnetic switches certainly help with consistency, but the case design and acoustic layering are doing most of the heavy lifting, resulting in a sound profile that makes you want to keep typing simply because it sounds so great.</p><h2 id="easily-the-sturdiest-keyboard-i-ve-ever-tested">Easily the sturdiest keyboard I’ve ever tested</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Z93GN4BHw4o7fHsXbdT29M" name="9357" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/Z93GN4BHw4o7fHsXbdT29M-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 build quality on the last MelGeek board I tested was already impressive, but the Centauri60 feels like a step up in every direction. Its machined aluminium chassis has that dense, rigid character you’d expect from a well‑cut block of metal, and there’s genuinely no flex anywhere in the frame. The tighter tolerances, cleaner seams, and more even weight distribution give it a more confident, premium feel — even compared to the already excellent Made68 Ultra V2.</p><p>Granted, I’ve only used two MelGeek keyboards to date, but it feels like there’s very little room for improvement when it comes to the Centauri60’s build quality. Compared to the Made68 Ultra V2, there’s a more deliberate weight to the board when you pick it up — 1.32kg (2.91 lbs) to be exact — so you know it won’t slide around your desk in the heat of online battle.</p><p>That weight distribution also contributes to a “single‑piece” feel in hand, something that really sells the mecha‑like character of the board. Nothing shifts, nothing creaks, and the chassis feels impeccably engineered in true Evangelion fashion. It’s so good, it could almost make Shinji’s father smile — if he were capable of such a thing.</p><h2 id="immaculate-styling-for-evangelion-fans">Immaculate styling for Evangelion fans</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xhZJFo89dpnr2EtosLooDM" name="9356" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/xhZJFo89dpnr2EtosLooDM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mentioned earlier, the Asuka Edition’s red colourway is easily its standout feature. It’s a saturated, confident red that really drives home its Evangelion inspiration. It’s unashamedly geeky, and its lighting helps amplify that personality: the south‑facing RGB shines cleanly through the keycaps. That bothered me somewhat on the Made68 Ultra V2, largely due to the dark purple text which made the keys hard to read. Here, the characters are white, making them far easier to make out at a glance.</p><p>Speaking of keycaps, the Asuka Edition employs a mix of red, orange and charcoal caps that mirror Asuka’s Unit‑02 palette, and the legends use Evangelion‑inspired typography that give the board a cool, futuristic vibe. The accent keys — particularly the green escape key and the darker modifiers — continues the anime‑driven look in a way that feels cohesive. Even the bottom of the case carries EVA‑specific detailing — the aforementioned “hidden” engraving — making the board feel like a collectible rather than just a themed variant.</p><p>The biggest downside of the entire keyboard sits right at the back: the USB‑C port. The port itself is recessed, but the real issue is the extra lip on MelGeek’s own USB‑C cable, which is designed to slot perfectly into that recess for a snug fit. Because of that proprietary lip, almost no third‑party cable can seat properly. So if you were hoping to pair the board with a coiled cable or any non‑MelGeek option, you’ll have to let that idea go or start thinking in terms of soldering irons. For a board that otherwise nails its visual identity, this restrictive setup feels out of place — and it’s the one styling choice that genuinely frustrated me.</p><h2 id="fully-specced-for-modern-gaming">Fully specced for modern gaming</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="iVfXBzmR645Z94LWunNCAM" name="20260818_163331" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/iVfXBzmR645Z94LWunNCAM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Centauri60’s magnetic switch system performs on par with any Hall‑effect board I’ve tested, delivering consistent, predictable keystrokes that can be tuned to impressively fine sensitivity. All the core features — rapid trigger, SOCD cleaning, adjustable actuation, analog‑style travel sensing, NKRO and zero‑dead‑zone behaviour — run smoothly and without any fuss.</p><p>Actuation can be adjusted from 0.1mm all the way to 3.4mm, giving you plenty of control whether you prefer hair‑trigger responsiveness or a more deliberate typing feel. Rapid Trigger is adjustable from 0.01mm to 2.5mm, resetting the moment a key starts to rise, which makes repeated inputs feel snappy and effortless. SOCD cleaning is handled in firmware and follows standard competitive rules, ensuring clean directional behaviour when opposite keys are pressed.</p><p>The switches also support full analog‑style travel sensing, with 0.01mm precision, allowing the board to read the exact depth of each keypress rather than just on/off states. In practice, this enables multi‑stage actions, variable movement speeds and pressure‑sensitive inputs in supported games. The keyboard runs at an 8,000Hz polling rate with a 0.125ms latency, and key response feels immediate whether you’re typing or gaming.</p><h2 id="man-s-ultimate-fighting-machine">Man’s ultimate fighting machine</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="StCvVfYNLdofq4uxrcRrDM" name="20260818_091708" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/StCvVfYNLdofq4uxrcRrDM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 I loved almost everything about MelGeek’s Made68 Ultra V2, the Centauri60 Evangelion ‘Asuka Edition’ improves on it in several of the areas which are most important to me. It’s got incredible styling that’s inspired by my favourite Evangelion character and EVA unit, its build quality feels strong enough to go into battle with Angels, its acoustics are deeper and thockier than that keyboard in a way that I find immensely satisfying, and its highly tunable Hall-effect switches give me all the high-end features I need in a competitive gaming keyboard. </p><p>As a noted fan of 75% layouts, I thought I’d be bothered by the Centauri60’s 60% layout, but it actually doesn’t feel too compact for me to use on a daily basis. And the fact that it still includes a small volume knob despite its condensed layout is a massive plus for me — especially since that was one of my biggest gripes with the Made68 Ultra V2.</p><p>The only real drawback is the USB‑C port, which is restrictive enough to be a genuine annoyance. That flaw aside, the Centauri60 becomes an easy recommendation for any Evangelion fan who wants a compact magnetic board with strong acoustics and a standout colourway.</p><div data-widget-type="multimodelreview" data-model-name="MelGeek Made68 Ultra V2,Melgeek Real 81,MelGeek Pixel" data-widget-title="Today's best MelGeek keyboard deals"></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/computing/peripherals/mechanical-keyboards-are-dead-heres-why-you-should-only-buy-a-magnetic-keyboard-for-gaming">Here’s why you should only buy a magnetic keyboard for gaming</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-almost-found-my-dream-keyboard-in-the-gravastar-mercury-v60-pro-if-it-werent-for-three-finicky-details">The GravaStar Mercury V60 Pro is almost perfect except for three finicky details</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/corsairs-vanguard-pro-96-keyboard-has-convinced-me-that-magnetic-switches-arent-just-hype">Corsair’s Vanguard Pro 96 keyboard has convinced me that magnetic switches aren’t just hype</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/this-bold-anime-themed-melgeek-centauri60-keyboard-just-became-my-daily-driver-heres-why</link>
                                                                            <description>
                            <![CDATA[ This Evangelion-themed board combines high-end Hall-effect performance with Cyberpunk style and mecha-grade build quality, and I can’t get enough of it. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">vNvke4qfWkFnhNP3fsR4Dd</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/2j6Ko6RgSK9oVAemx2rZFM-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 15 Sep 2026 03:32:29 +0000</pubDate>                                                                                                                                <updated>Tue, 15 Sep 2026 05:17:52 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                                                                                    <dc:creator><![CDATA[ Stephen Lambrechts ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/JN3jEQpqyyt32as8AJJqXe-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Stephen Lambrechts is the Managing Editor of Tom&#039;s Guide AU and has written professionally across the categories of tech, film, television and gaming for the last 15 years. Before Tom&#039;s Guide, he spent several years as a Senior Journalist at TechRadar, had a brief stint as Editor in Chief at Official Xbox Magazine Australia, and worked at IGN. He&#039;s also written for such mags and sites as APC, TechLife Australia, T3, FilmInk, Daily Telegraph and AskMen. Additionally, he&#039;s a Tomatometer-approved critic on Rotten Tomatoes, and has offered his tech advice on radio and television.&lt;/p&gt;&lt;p&gt;He&#039;s an expert when it comes to smartphones, TVs, gaming and streaming, having contributed many reviews in each of those categories over the years. Despite his many articles on streaming, he&#039;s a passionate collector of physical media who enjoys listening to vinyl records and acquiring obscure horror movies from the &#039;60s, &#039;70s and 80s. He can usually be seen rocking the latest retro sneaker releases, particularly Air Jordans and Reebok Pumps. Occasionally, he also indulges in other non-hipster stuff, like hiking.&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/2j6Ko6RgSK9oVAemx2rZFM-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[MelGeek x Evangelion Centauri60 &#039;Asuka Edition&#039; keyboard]]></media:description>                                                            <media:text><![CDATA[MelGeek x Evangelion Centauri60 &#039;Asuka Edition&#039; keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[MelGeek x Evangelion Centauri60 &#039;Asuka Edition&#039; keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/2j6Ko6RgSK9oVAemx2rZFM-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Having recently spent time with MelGeek’s Made68 Ultra V2 keyboard, I came away declaring my love for its <a href="https://www.tomsguide.com/computing/keyboards/ive-been-using-melgeeks-made68-ultra-v2-for-three-weeks-and-im-in-love-with-its-magnetic-switches-slick-styling-and-clean-sound">magnetic switches, slick styling and clean sound</a>. With those things fresh in my mind, I approached the <a href="https://www.melgeek.com/en-eu/pages/melgeek-x-evangelion-centauri60-asuka-edition" target="_blank" rel="nofollow">Centauri60 Evangelion ‘Asuka Edition’</a> expecting a familiar experience. What I found instead was a keyboard that carries over everything I liked about the Ultra V2 while improving the parts that matter most to me in everyday use.</p><p>The Asuka Edition isn’t exactly subtle — it leans into its Evangelion theme and makes no effort to hide it. You get a ‘dominant’ red case, sharp angled edges, lighting strips on both sides, a hidden engraving of Asuka’s most iconic line, a Prog‑Knife motif, a metal nameplate built into the frame, and a laser‑etched EVA‑02 insignia. It’s unapologetically bold and openly geeky, far more so than the Made68 Ultra V2 I tested earlier, and that personality hits you straight away.</p><p>And yes, the volume knob is still one of those small touches that makes a big impact. On a 60% board, having a tactile media control feels like a luxury, and it’s one I’m thankful for given its absence from the Made 68 Ultra V2. Of course, there’s more to the Centauri60 than its theme —  it’s a keyboard which so resoundingly nails its build, sound, feel, gaming features and typing experience, that it’s now become my daily driver at home — here’s why.</p><h2 id="a-thocking-spectacular-sound-profile">A thocking spectacular sound profile</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hbjiSdJGBPczWwBjHoMfLM" name="9365" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/hbjiSdJGBPczWwBjHoMfLM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 acoustic profile is where the Centauri60 surprised me most. The Ultra V2 I tested sounded super clean, but it used KOM Lite switches which resulted in a brighter, snappier sound signature. The Centauri60, by contrast, comes installed with TTC Flip King magnetic switches, which I instantly preferred for their deeper, more muted tone. </p><p>Another major contributor has to be its CNC aluminium unibody, which weighs a hefty 1.32kg and provides a dense, vibration‑absorbing foundation. Compared to some of the more plastic-centric keyboards I’ve used in the past, this heavy-duty body noticeably suppresses higher‑frequency noise while letting lower tones develop more fully, giving the board a richer, more grounded sound. </p><p>The Centauri60 also uses a five‑layer acoustic dampening system — a combination of Poron foam, PET film, and a leaf‑spring gasket mount — designed to eliminate hollow cavities and absorb unwanted resonance. The result, in my estimation, is a keyboard that minimises harsh clacks for a smoother, more “creamy” signature</p><p>The magnetic switches certainly help with consistency, but the case design and acoustic layering are doing most of the heavy lifting, resulting in a sound profile that makes you want to keep typing simply because it sounds so great.</p><h2 id="easily-the-sturdiest-keyboard-i-ve-ever-tested">Easily the sturdiest keyboard I’ve ever tested</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Z93GN4BHw4o7fHsXbdT29M" name="9357" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/Z93GN4BHw4o7fHsXbdT29M-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 build quality on the last MelGeek board I tested was already impressive, but the Centauri60 feels like a step up in every direction. Its machined aluminium chassis has that dense, rigid character you’d expect from a well‑cut block of metal, and there’s genuinely no flex anywhere in the frame. The tighter tolerances, cleaner seams, and more even weight distribution give it a more confident, premium feel — even compared to the already excellent Made68 Ultra V2.</p><p>Granted, I’ve only used two MelGeek keyboards to date, but it feels like there’s very little room for improvement when it comes to the Centauri60’s build quality. Compared to the Made68 Ultra V2, there’s a more deliberate weight to the board when you pick it up — 1.32kg (2.91 lbs) to be exact — so you know it won’t slide around your desk in the heat of online battle.</p><p>That weight distribution also contributes to a “single‑piece” feel in hand, something that really sells the mecha‑like character of the board. Nothing shifts, nothing creaks, and the chassis feels impeccably engineered in true Evangelion fashion. It’s so good, it could almost make Shinji’s father smile — if he were capable of such a thing.</p><h2 id="immaculate-styling-for-evangelion-fans">Immaculate styling for Evangelion fans</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xhZJFo89dpnr2EtosLooDM" name="9356" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/xhZJFo89dpnr2EtosLooDM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mentioned earlier, the Asuka Edition’s red colourway is easily its standout feature. It’s a saturated, confident red that really drives home its Evangelion inspiration. It’s unashamedly geeky, and its lighting helps amplify that personality: the south‑facing RGB shines cleanly through the keycaps. That bothered me somewhat on the Made68 Ultra V2, largely due to the dark purple text which made the keys hard to read. Here, the characters are white, making them far easier to make out at a glance.</p><p>Speaking of keycaps, the Asuka Edition employs a mix of red, orange and charcoal caps that mirror Asuka’s Unit‑02 palette, and the legends use Evangelion‑inspired typography that give the board a cool, futuristic vibe. The accent keys — particularly the green escape key and the darker modifiers — continues the anime‑driven look in a way that feels cohesive. Even the bottom of the case carries EVA‑specific detailing — the aforementioned “hidden” engraving — making the board feel like a collectible rather than just a themed variant.</p><p>The biggest downside of the entire keyboard sits right at the back: the USB‑C port. The port itself is recessed, but the real issue is the extra lip on MelGeek’s own USB‑C cable, which is designed to slot perfectly into that recess for a snug fit. Because of that proprietary lip, almost no third‑party cable can seat properly. So if you were hoping to pair the board with a coiled cable or any non‑MelGeek option, you’ll have to let that idea go or start thinking in terms of soldering irons. For a board that otherwise nails its visual identity, this restrictive setup feels out of place — and it’s the one styling choice that genuinely frustrated me.</p><h2 id="fully-specced-for-modern-gaming">Fully specced for modern gaming</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="iVfXBzmR645Z94LWunNCAM" name="20260818_163331" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/iVfXBzmR645Z94LWunNCAM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Centauri60’s magnetic switch system performs on par with any Hall‑effect board I’ve tested, delivering consistent, predictable keystrokes that can be tuned to impressively fine sensitivity. All the core features — rapid trigger, SOCD cleaning, adjustable actuation, analog‑style travel sensing, NKRO and zero‑dead‑zone behaviour — run smoothly and without any fuss.</p><p>Actuation can be adjusted from 0.1mm all the way to 3.4mm, giving you plenty of control whether you prefer hair‑trigger responsiveness or a more deliberate typing feel. Rapid Trigger is adjustable from 0.01mm to 2.5mm, resetting the moment a key starts to rise, which makes repeated inputs feel snappy and effortless. SOCD cleaning is handled in firmware and follows standard competitive rules, ensuring clean directional behaviour when opposite keys are pressed.</p><p>The switches also support full analog‑style travel sensing, with 0.01mm precision, allowing the board to read the exact depth of each keypress rather than just on/off states. In practice, this enables multi‑stage actions, variable movement speeds and pressure‑sensitive inputs in supported games. The keyboard runs at an 8,000Hz polling rate with a 0.125ms latency, and key response feels immediate whether you’re typing or gaming.</p><h2 id="man-s-ultimate-fighting-machine">Man’s ultimate fighting machine</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="StCvVfYNLdofq4uxrcRrDM" name="20260818_091708" alt="MelGeek x Evangelion Centauri60 'Asuka Edition' keyboard" src="https://cdn.mos.cms.futurecdn.net/StCvVfYNLdofq4uxrcRrDM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 I loved almost everything about MelGeek’s Made68 Ultra V2, the Centauri60 Evangelion ‘Asuka Edition’ improves on it in several of the areas which are most important to me. It’s got incredible styling that’s inspired by my favourite Evangelion character and EVA unit, its build quality feels strong enough to go into battle with Angels, its acoustics are deeper and thockier than that keyboard in a way that I find immensely satisfying, and its highly tunable Hall-effect switches give me all the high-end features I need in a competitive gaming keyboard. </p><p>As a noted fan of 75% layouts, I thought I’d be bothered by the Centauri60’s 60% layout, but it actually doesn’t feel too compact for me to use on a daily basis. And the fact that it still includes a small volume knob despite its condensed layout is a massive plus for me — especially since that was one of my biggest gripes with the Made68 Ultra V2.</p><p>The only real drawback is the USB‑C port, which is restrictive enough to be a genuine annoyance. That flaw aside, the Centauri60 becomes an easy recommendation for any Evangelion fan who wants a compact magnetic board with strong acoustics and a standout colourway.</p><div data-widget-type="multimodelreview" data-model-name="MelGeek Made68 Ultra V2,Melgeek Real 81,MelGeek Pixel" data-widget-title="Today's best MelGeek keyboard deals"></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/computing/peripherals/mechanical-keyboards-are-dead-heres-why-you-should-only-buy-a-magnetic-keyboard-for-gaming">Here’s why you should only buy a magnetic keyboard for gaming</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-almost-found-my-dream-keyboard-in-the-gravastar-mercury-v60-pro-if-it-werent-for-three-finicky-details">The GravaStar Mercury V60 Pro is almost perfect except for three finicky details</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/corsairs-vanguard-pro-96-keyboard-has-convinced-me-that-magnetic-switches-arent-just-hype">Corsair’s Vanguard Pro 96 keyboard has convinced me that magnetic switches aren’t just hype</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I’ve loved typing on the Tecware Spectre 75 — but it falls flat in the face of fierce competition ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’ve tested nearly 100 keyboards, at the time of writing, and the Tecware Spectre 75 is one I’ve boxed back up the quickest. The keyboard costs just $79, and offers a great typing experience. It’s great for hardcore typists who want nothing but responsive switches for a day’s worth of work — but its issues are too hard to ignore.</p><p>The Spectre 75 features a built-in LCD screen you can display GIFs on, but frustratingly, its companion software (needed for customizing the screen) is finicky to use. The build and design are nothing to write home about, neither is its limited stock switch choice. But my biggest issue with the Spectre 75 is how it becomes unresponsive after being in sleep mode.</p><p>To find out why it <em>isn’t </em>one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a>, read my full Tecware Spectre 75 review.</p><h2 class="article-body__section" id="section-tecware-spectre-75-review-specs"><span>Tecware Spectre 75 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/TECWARE-Mechanical-Customizable-Multi-Function-Hot-Swappable/dp/B0DGY4QWZY/" target="_blank" rel="nofollow">$79</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Tecware Cocoa Brown (tactile) or Candy Red (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 81 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>4,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.99 x 5.71 x 1.50 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.87lbs</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-tecware-spectre-75-review-cheat-sheet"><span>Tecware Spectre 75 review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 75% layout, hot-swappable mechanical keyboard with either tactile or linear switches</li><li><strong>Who is it for? </strong>For hardcore typists and casual gamers</li><li><strong>How much does it cost? </strong>The Tecware Spectre 75 costs just <a href="https://www.amazon.com/TECWARE-Mechanical-Customizable-Multi-Function-Hot-Swappable/dp/B0DGY4QWZY/" target="_blank" rel="nofollow">$79</a></li><li><strong>What do we like?</strong> The typing experience, LCD screen to display GIFs on, and 1KHz polling for gaming</li><li><strong>What don’t we like?</strong> The plasticky build, the lackluster design, janky companion software, and limited stock switch choice</li></ul><h2 class="article-body__section" id="section-tecware-spectre-75-review-the-ups"><span>Tecware Spectre 75 review: The ups</span></h2><p>The Tecware Spectre 75 is lovely to type on, it sounds good, and it features a built-in LCD screen you can display GIFs on. It’s a decent keyboard for gaming, too, thanks to 1KHz polling.</p><h2 id="lovely-to-type-on">Lovely to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SuJZUSvgXbVpju7N3tqxw7" name="Tecware_Spectre75_Keyboard_002.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/SuJZUSvgXbVpju7N3tqxw7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 is a fantastic keyboard to type on, so it’s great for hardcore typists (like myself). The keyboard comes fitted with pre-lubed Cocoa Brown (tactile) or Candy Red (linear) mechanical switches. I tested the Candy Reds which require 45g of force to activate. They’re slightly heavier than the 35g Sea Salt linears in the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115) but even so, I’ve found the Candy Reds non-fatiguing to type on. I wrote this review <em>and </em>another on the Spectre 75!</p><p>The Candy Reds feel smooth and responsive to type on. Combined with the double-shot PBT keycaps, the gasket mount, and several layers of foam padding, the Spectre 75 sounds lovely, too. It’s got a deep, creamy sound which isn’t too loud that it disturbs your coworkers, but it isn’t too quiet either, so you can appreciate it.</p><p>Speaking of, the double-shot PBT keycaps feel soft to touch. By their nature, the PBT keycaps are more resistant to oil and wear and tear than their ABS counterparts. They don’t get shiny like the ones on the expensive <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb5-review">Turtle Beach Command Series KB5</a> ($149) either.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Tecware Spectre 75</p></td><td  ><p>93</p></td><td  ><p>90%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb5-review">Turtle Beach Command Series KB5</a></p></td><td  ><p>108</p></td><td  ><p>97%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/mchose-k99-v2-review">MCHOSE K99 V2</a></p></td><td  ><p>98</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">MCHOSE X75 V2</a></p></td><td  ><p>94</p></td><td  ><p>95.37%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100 Pro</a></p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.84</p></td><td  ><p>91.24%</p></td></tr></tbody></table></div><p>As soon as I unboxed the Spectre 75, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> (as I always do). Straight out of the box, I was able to achieve 93 words per minute with 90% accuracy. Though a little slower than what I’m used to, like 109WPM on the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139), I’m happy anytime I get over 90WPM, so I’m pleased with the Spectre 75’s performance.</p><h2 id="good-gaming-performance">Good gaming 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="H5wq4jPf62QhSNGkdvEtw7" name="Tecware_Spectre75_Keyboard_008.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/H5wq4jPf62QhSNGkdvEtw7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 is a competent gaming keyboard, packing a 1,000Hz polling rate over a wired connection. Over Bluetooth, this naturally drops to just 125Hz. 1KHz polling is more than plenty for 99% of gamers. I played Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a>, and I didn’t notice any latency issues or input lag.</p><p>Linear switches are usually great for gaming, and the same is true for the Spectre 75’s Candy Reds. I found them extremely responsive in my testing. Tecware doesn’t specify the switches’ pre-travel distance, or how far down the switches need to be pressed to activate them. Even so, I felt they only required light taps to activate.</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="QVDvJgrDvwRv7iFuAWo4j7" name="Tecware_Spectre75_Keyboard_007.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/QVDvJgrDvwRv7iFuAWo4j7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 also get N-key rollover which ensures consistent and accurate key registration when you’re typing and playing. You can basically hold down several keys at once and all of them will be registered by the keyboard’s hardware and executed on screen. In Cyberpunk 2077, I was able to sprint, jump, reload my weapon and activate my Sandevistan cyberware to slow down time, all at once. This gave me a slight competitive advantage and I was able to run circles around enemies.</p><p>For competitive gamers or esports players, I’d recommend a Hall Effect keyboard with adjustable actuation and 8,000Hz polling, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> ($59) or the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> ($99). If you’re a casual gamer, though, the Spectre 75 should more than suffice.</p><h2 id="customizable-lcd-screen">Customizable LCD screen</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="G3Vy5jG3agzGy5ofvxPnQ7" name="Tecware_Spectre75_Keyboard_006.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/G3Vy5jG3agzGy5ofvxPnQ7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 I had a nickel for every keyboard I’ve tested that features a built-in LCD screen, I’d have <em>many. </em>Similar to the likes of the Turtle Beach Command Series KB5, the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-rt100-pro-review">Epomaker RT100 Pro</a> ($119) and the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> ($259), the Tecware Spectre 75 features a customizable LCD screen.</p><p>Located between the volume knob and Function row, the screen is tiny but bright. By default, it displays a pre-installed GIF, and the knob lets you adjust the volume. If you hold down Fn and the knob, you can enter LCD Menu Navigation mode. Twisting the knob thereafter lets you cycle through the keyboard’s various settings. You can use the screen (and the knob) to adjust the RGB lighting’s effects, speed and brightness, and swap between macOS and Windows layouts.</p><p>I found the screen responsive in my testing — it's the Spectre 75’s companion software (that’s needed to upload GIFs) that I have a problem with. More on that later, though.</p><h2 id="low-price">Low price</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WALhtUQFzxoYNmDXPyyXY7" name="Tecware_Spectre75_Keyboard_009.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/WALhtUQFzxoYNmDXPyyXY7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 best things about the Tecware Spectre 75 is that it doesn’t cost an arm and a leg. It retails for just <a href="https://www.amazon.com/TECWARE-Mechanical-Customizable-Multi-Function-Hot-Swappable/dp/B0DGY4QWZY/" target="_blank" rel="nofollow">$79 at Amazon U.S.</a> which makes it one of the cheapest keyboards I’ve tested so far. It’s cheaper than some of our favorite budget mech decks, like the <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). It’s priced similarly to the <a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a> ($79) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a> ($74) — though the latter is wired-only.</p><h2 class="article-body__section" id="section-tecware-spectre-75-review-the-downs"><span>Tecware Spectre 75 review: The downs</span></h2><p>The Tecware Spectre 75 isn’t perfect. Its lackluster design, plasticky build, janky companion software, and limited stock switch choice leave a lot to be desired.</p><h2 id="lackluster-design">Lackluster 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="jJxZn6see5rSbBkYD2ybu7" name="Tecware_Spectre75_Keyboard_010.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/jJxZn6see5rSbBkYD2ybu7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 preface this by saying that there’s nothing wrong with the Tecware Spectre 75’s design — it’s just very dull and very boring to look at. There’s nothing special about the design. The Spectre 75 comes in either black or white, and they’re… fine? This is a decent-looking keyboard for anyone who prefers something low-key. There’s flashy per-key RGB lighting that adds a splash of color, though.</p><p>If you want a keyboard that pops, or utilizes a combination of different colors at the least, I’d recommend the Royal Kludge L75, the <a href="https://www.tomsguide.com/computing/peripherals/gravastar-mercury-k1-lite-review">GravaStar Mercury K1 Lite</a> ($89) or the <a href="https://www.tomsguide.com/computing/keyboards/mchose-k99-v2-review">MCHOSE K99 V2</a> ($99). Of those, the Mercury K1 Lite is the polar opposite of the Spectre 75, with a garish, loud design. I’d stick to the Royal Kludge L75 if I were you.</p><h2 id="plasticky-build">Plasticky build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NZLSDFfXjWCEaiF9YrUzw7" name="Tecware_Spectre75_Keyboard_003.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/NZLSDFfXjWCEaiF9YrUzw7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 isn’t the best built keyboard out there. Its body is made entirely of plastic which feels a little <em>too </em>cheap for my liking. It looks like a reskinned <a href="https://www.tomsguide.com/computing/peripherals/keychron-v1-review">Keychron V1</a> ($94) but without the steel plate. The plastic case feels slightly flimsy, and there’s some flex when you twist the Spectre 75 by its sides. It does feature a metallic knob in the top-right corner which feels premium, though.</p><p>Its all-plastic build makes the keyboard fairly light, though, so it’s good to travel with. The Spectre 75 weighs 1.87lbs so it’s lighter than the Keychron V1 8K (2.19lbs). But if you want an even lighter keyboard that also feels premium, I’d recommend the NuPhy Node75 (1.45lbs).</p><h2 id="janky-companion-software">Janky companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/g5Y8iboYraqfreASfZa7Pj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/RQ7VBxgnBQ9Pah9LFGEyJj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/gsttqmD2q368XMvXnQbQfj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/WGrAJxKtHr2CBHe38RiWCj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure></figure><p>I’m no stranger to janky companion software, having tested many gadgets with finicky apps, but <em>great snakes </em>is the Tecware Spectre 75’s app infuriating. You’ll need to download the <a href="https://www.tecware.co/software" target="_blank" rel="nofollow">Spectre 75’s dedicated software</a> from the Tecware website, and it’s not available on macOS, even though the keyboard itself is compatible with Apple’s laptops. If you own a MacBook, you won’t be able to customize the LCD screen or upload GIFs to it — it’s worth considering a keyboard like the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-rt100-pro-review">Epomaker RT75</a> ($91) instead.</p><p>Speaking of GIFs, uploading one to the Spectre 75 feels like an uphill climb, or like Sisyphus pushing a heavy boulder up said hill, only to watch it roll back down. The process seems simple enough at first. Navigate to the GIF tab, click on the + icon, and add your preferred GIF. When I did so, the software told me that the GIF had 61 frames and only 44 were allowed. You can delete specific frames within the software itself, but you can’t do so in bulk. You have to delete each frame individually — and this includes confirming <em>every time </em>that you’re certain you want to delete said frame.</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="tbriHkw2sqUbFGkCwBfYWm" name="Tecware_Spectre_75" alt="Tecware Spectre 75 keyboard's screen" src="https://cdn.mos.cms.futurecdn.net/tbriHkw2sqUbFGkCwBfYWm-1920-80.jpg" mos="" align="middle" fullscreen="1" width="2048" height="1152" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/tbriHkw2sqUbFGkCwBfYWm-1920-80.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: Tom's Guide)</span></figcaption></figure><p>It took some time, but I managed to trim the frames down to 44. I clicked ‘Upload to Keyboard’ and watched the progress bar fill up… and then the software crashed. I booted up the software again and repeated the process, only this time the software didn’t say that the frames were limited to 44 — strange, but at least it saves me a gazillion steps. Again, I clicked ‘Upload to Keyboard’ and this time, it worked (thankfully).</p><p>But that isn’t where my troubles stopped. I wanted to test the software’s macro recording feature but for some reason, none of the options in the macro tab were clickable. I reinstalled the software and the issue persisted. I even checked if the Spectre 75 had the latest firmware installed (it did).</p><p>The Spectre 75’s LCD screen is capable of displaying CPU and GPU stats, too, but the software needs to be running in the background — again, impossible to do on macOS. Overall, the software left a sour taste in my mouth, and it sticks out like a sore thumb.</p><h2 id="limited-stock-switch-choice">Limited stock switch choice</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tSoN6eKP7rSzQd8sqqY5v6" name="Tecware_Spectre75_Keyboard_012.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/tSoN6eKP7rSzQd8sqqY5v6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 comes with pre-lubed switches that are made in-house. Unfortunately, you’re limited to just two stock switch sets. You can choose either Cocoa Brown tactile or Candy Red linear switches. There are no clicky or silent switches, and you’ll have to buy your preferred ones separately.</p><p>Thankfully, the Spectre 75 is hot-swappable and compatible with south-facing 5-pin mechanical switches. It’s nice to see a budget keyboard offering hot-swappability when some pricey keyboards don’t (looking at you, Turtle Beach Command Series KB5). If you want more stock switch choice at checkout, I’d recommend the Keychron V1 8K.</p><h2 id="not-the-most-responsive-when-waking">Not the most responsive when waking</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bnVYxtLuhSJSPAodxiFxK7" name="Tecware_Spectre75_Keyboard_013.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/bnVYxtLuhSJSPAodxiFxK7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 last nail in the Tecware Spectre 75’s coffin is its tendency to not respond when it’s waking from sleep mode. In my testing, over Bluetooth and a wired connection, the keyboard wouldn’t respond after my laptop — and therefore, the Spectre 75 — had been asleep for a while. Though it didn’t happen <em>all </em>the time, I had to reconnect the keyboard a few times to get it working again. I tested this on macOS and Windows machines and had the same issue each time.</p><p>I haven’t had this issue with other mechanical keyboards I’ve tested. For that reason, and everything else I’ve mentioned up until now, I’d recommend the Keychron V1 8K or the Royal Kludge L75. I’ve tested both keyboards extensively and neither of them have given me this much grief.</p><p>I've reached out to Tecware's representative about this issue, to see whether it's limited to my review unit alone.</p><h2 class="article-body__section" id="section-tecware-spectre-75-review-verdict"><span>Tecware Spectre 75 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="hkNmhewXwLrRkkaVw6iww7" name="Tecware_Spectre75_Keyboard_005.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/hkNmhewXwLrRkkaVw6iww7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 is a decent mechanical keyboard for typing and casual gaming, thanks to its responsive linear switches and 1,000Hz polling rate. It features an LCD screen that you can view keyboard and system stats on, or display GIFs on, too.</p><p>But there isn’t much else to like about the Spectre 75. The companion software needed to customize the screen is janky and frustrating to use, while the keyboard itself sports a lackluster design and plasticky build. You’re limited to just two stock switch sets, too. I also struggled with the keyboard becoming unresponsive in my testing.</p><p>While $79 isn’t a high asking price, you can get better value for your money elsewhere. The Keychron V1 8K and the Royal Kludge L75 are fantastic keyboards under $80, and are much better choices if you want a feature-packed deck.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/tecware-spectre-75-review</link>
                                                                            <description>
                            <![CDATA[ The Tecware Spectre 75 is a decent keyboard for typing, but issues like janky software, a plasticky build, and unresponsiveness when waking are hard to ignore. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">WDnW4xchRmVYnTZ3JMVESj</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/pHsQyR36NJTSNkPe8L9tX6-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 11 Sep 2026 12:16:14 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/pHsQyR36NJTSNkPe8L9tX6-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Tecware Spectre 75 mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A black Tecware Spectre 75 mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A black Tecware Spectre 75 mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/pHsQyR36NJTSNkPe8L9tX6-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’ve tested nearly 100 keyboards, at the time of writing, and the Tecware Spectre 75 is one I’ve boxed back up the quickest. The keyboard costs just $79, and offers a great typing experience. It’s great for hardcore typists who want nothing but responsive switches for a day’s worth of work — but its issues are too hard to ignore.</p><p>The Spectre 75 features a built-in LCD screen you can display GIFs on, but frustratingly, its companion software (needed for customizing the screen) is finicky to use. The build and design are nothing to write home about, neither is its limited stock switch choice. But my biggest issue with the Spectre 75 is how it becomes unresponsive after being in sleep mode.</p><p>To find out why it <em>isn’t </em>one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a>, read my full Tecware Spectre 75 review.</p><h2 class="article-body__section" id="section-tecware-spectre-75-review-specs"><span>Tecware Spectre 75 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/TECWARE-Mechanical-Customizable-Multi-Function-Hot-Swappable/dp/B0DGY4QWZY/" target="_blank" rel="nofollow">$79</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Tecware Cocoa Brown (tactile) or Candy Red (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 81 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>4,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.99 x 5.71 x 1.50 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.87lbs</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-tecware-spectre-75-review-cheat-sheet"><span>Tecware Spectre 75 review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 75% layout, hot-swappable mechanical keyboard with either tactile or linear switches</li><li><strong>Who is it for? </strong>For hardcore typists and casual gamers</li><li><strong>How much does it cost? </strong>The Tecware Spectre 75 costs just <a href="https://www.amazon.com/TECWARE-Mechanical-Customizable-Multi-Function-Hot-Swappable/dp/B0DGY4QWZY/" target="_blank" rel="nofollow">$79</a></li><li><strong>What do we like?</strong> The typing experience, LCD screen to display GIFs on, and 1KHz polling for gaming</li><li><strong>What don’t we like?</strong> The plasticky build, the lackluster design, janky companion software, and limited stock switch choice</li></ul><h2 class="article-body__section" id="section-tecware-spectre-75-review-the-ups"><span>Tecware Spectre 75 review: The ups</span></h2><p>The Tecware Spectre 75 is lovely to type on, it sounds good, and it features a built-in LCD screen you can display GIFs on. It’s a decent keyboard for gaming, too, thanks to 1KHz polling.</p><h2 id="lovely-to-type-on">Lovely to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SuJZUSvgXbVpju7N3tqxw7" name="Tecware_Spectre75_Keyboard_002.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/SuJZUSvgXbVpju7N3tqxw7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 is a fantastic keyboard to type on, so it’s great for hardcore typists (like myself). The keyboard comes fitted with pre-lubed Cocoa Brown (tactile) or Candy Red (linear) mechanical switches. I tested the Candy Reds which require 45g of force to activate. They’re slightly heavier than the 35g Sea Salt linears in the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115) but even so, I’ve found the Candy Reds non-fatiguing to type on. I wrote this review <em>and </em>another on the Spectre 75!</p><p>The Candy Reds feel smooth and responsive to type on. Combined with the double-shot PBT keycaps, the gasket mount, and several layers of foam padding, the Spectre 75 sounds lovely, too. It’s got a deep, creamy sound which isn’t too loud that it disturbs your coworkers, but it isn’t too quiet either, so you can appreciate it.</p><p>Speaking of, the double-shot PBT keycaps feel soft to touch. By their nature, the PBT keycaps are more resistant to oil and wear and tear than their ABS counterparts. They don’t get shiny like the ones on the expensive <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb5-review">Turtle Beach Command Series KB5</a> ($149) either.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Tecware Spectre 75</p></td><td  ><p>93</p></td><td  ><p>90%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb5-review">Turtle Beach Command Series KB5</a></p></td><td  ><p>108</p></td><td  ><p>97%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/mchose-k99-v2-review">MCHOSE K99 V2</a></p></td><td  ><p>98</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">MCHOSE X75 V2</a></p></td><td  ><p>94</p></td><td  ><p>95.37%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100 Pro</a></p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.84</p></td><td  ><p>91.24%</p></td></tr></tbody></table></div><p>As soon as I unboxed the Spectre 75, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> (as I always do). Straight out of the box, I was able to achieve 93 words per minute with 90% accuracy. Though a little slower than what I’m used to, like 109WPM on the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139), I’m happy anytime I get over 90WPM, so I’m pleased with the Spectre 75’s performance.</p><h2 id="good-gaming-performance">Good gaming 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="H5wq4jPf62QhSNGkdvEtw7" name="Tecware_Spectre75_Keyboard_008.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/H5wq4jPf62QhSNGkdvEtw7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 is a competent gaming keyboard, packing a 1,000Hz polling rate over a wired connection. Over Bluetooth, this naturally drops to just 125Hz. 1KHz polling is more than plenty for 99% of gamers. I played Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a>, and I didn’t notice any latency issues or input lag.</p><p>Linear switches are usually great for gaming, and the same is true for the Spectre 75’s Candy Reds. I found them extremely responsive in my testing. Tecware doesn’t specify the switches’ pre-travel distance, or how far down the switches need to be pressed to activate them. Even so, I felt they only required light taps to activate.</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="QVDvJgrDvwRv7iFuAWo4j7" name="Tecware_Spectre75_Keyboard_007.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/QVDvJgrDvwRv7iFuAWo4j7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 also get N-key rollover which ensures consistent and accurate key registration when you’re typing and playing. You can basically hold down several keys at once and all of them will be registered by the keyboard’s hardware and executed on screen. In Cyberpunk 2077, I was able to sprint, jump, reload my weapon and activate my Sandevistan cyberware to slow down time, all at once. This gave me a slight competitive advantage and I was able to run circles around enemies.</p><p>For competitive gamers or esports players, I’d recommend a Hall Effect keyboard with adjustable actuation and 8,000Hz polling, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> ($59) or the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> ($99). If you’re a casual gamer, though, the Spectre 75 should more than suffice.</p><h2 id="customizable-lcd-screen">Customizable LCD screen</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="G3Vy5jG3agzGy5ofvxPnQ7" name="Tecware_Spectre75_Keyboard_006.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/G3Vy5jG3agzGy5ofvxPnQ7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 I had a nickel for every keyboard I’ve tested that features a built-in LCD screen, I’d have <em>many. </em>Similar to the likes of the Turtle Beach Command Series KB5, the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-rt100-pro-review">Epomaker RT100 Pro</a> ($119) and the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> ($259), the Tecware Spectre 75 features a customizable LCD screen.</p><p>Located between the volume knob and Function row, the screen is tiny but bright. By default, it displays a pre-installed GIF, and the knob lets you adjust the volume. If you hold down Fn and the knob, you can enter LCD Menu Navigation mode. Twisting the knob thereafter lets you cycle through the keyboard’s various settings. You can use the screen (and the knob) to adjust the RGB lighting’s effects, speed and brightness, and swap between macOS and Windows layouts.</p><p>I found the screen responsive in my testing — it's the Spectre 75’s companion software (that’s needed to upload GIFs) that I have a problem with. More on that later, though.</p><h2 id="low-price">Low price</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WALhtUQFzxoYNmDXPyyXY7" name="Tecware_Spectre75_Keyboard_009.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/WALhtUQFzxoYNmDXPyyXY7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 best things about the Tecware Spectre 75 is that it doesn’t cost an arm and a leg. It retails for just <a href="https://www.amazon.com/TECWARE-Mechanical-Customizable-Multi-Function-Hot-Swappable/dp/B0DGY4QWZY/" target="_blank" rel="nofollow">$79 at Amazon U.S.</a> which makes it one of the cheapest keyboards I’ve tested so far. It’s cheaper than some of our favorite budget mech decks, like the <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). It’s priced similarly to the <a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a> ($79) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a> ($74) — though the latter is wired-only.</p><h2 class="article-body__section" id="section-tecware-spectre-75-review-the-downs"><span>Tecware Spectre 75 review: The downs</span></h2><p>The Tecware Spectre 75 isn’t perfect. Its lackluster design, plasticky build, janky companion software, and limited stock switch choice leave a lot to be desired.</p><h2 id="lackluster-design">Lackluster 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="jJxZn6see5rSbBkYD2ybu7" name="Tecware_Spectre75_Keyboard_010.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/jJxZn6see5rSbBkYD2ybu7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 preface this by saying that there’s nothing wrong with the Tecware Spectre 75’s design — it’s just very dull and very boring to look at. There’s nothing special about the design. The Spectre 75 comes in either black or white, and they’re… fine? This is a decent-looking keyboard for anyone who prefers something low-key. There’s flashy per-key RGB lighting that adds a splash of color, though.</p><p>If you want a keyboard that pops, or utilizes a combination of different colors at the least, I’d recommend the Royal Kludge L75, the <a href="https://www.tomsguide.com/computing/peripherals/gravastar-mercury-k1-lite-review">GravaStar Mercury K1 Lite</a> ($89) or the <a href="https://www.tomsguide.com/computing/keyboards/mchose-k99-v2-review">MCHOSE K99 V2</a> ($99). Of those, the Mercury K1 Lite is the polar opposite of the Spectre 75, with a garish, loud design. I’d stick to the Royal Kludge L75 if I were you.</p><h2 id="plasticky-build">Plasticky build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NZLSDFfXjWCEaiF9YrUzw7" name="Tecware_Spectre75_Keyboard_003.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/NZLSDFfXjWCEaiF9YrUzw7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 isn’t the best built keyboard out there. Its body is made entirely of plastic which feels a little <em>too </em>cheap for my liking. It looks like a reskinned <a href="https://www.tomsguide.com/computing/peripherals/keychron-v1-review">Keychron V1</a> ($94) but without the steel plate. The plastic case feels slightly flimsy, and there’s some flex when you twist the Spectre 75 by its sides. It does feature a metallic knob in the top-right corner which feels premium, though.</p><p>Its all-plastic build makes the keyboard fairly light, though, so it’s good to travel with. The Spectre 75 weighs 1.87lbs so it’s lighter than the Keychron V1 8K (2.19lbs). But if you want an even lighter keyboard that also feels premium, I’d recommend the NuPhy Node75 (1.45lbs).</p><h2 id="janky-companion-software">Janky companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/g5Y8iboYraqfreASfZa7Pj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/RQ7VBxgnBQ9Pah9LFGEyJj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/gsttqmD2q368XMvXnQbQfj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/WGrAJxKtHr2CBHe38RiWCj-1920-80.jpg" alt="Tecware Spectre 75 companion software screenshot" /><figcaption><small role="credit">Tecware</small></figcaption></figure></figure><p>I’m no stranger to janky companion software, having tested many gadgets with finicky apps, but <em>great snakes </em>is the Tecware Spectre 75’s app infuriating. You’ll need to download the <a href="https://www.tecware.co/software" target="_blank" rel="nofollow">Spectre 75’s dedicated software</a> from the Tecware website, and it’s not available on macOS, even though the keyboard itself is compatible with Apple’s laptops. If you own a MacBook, you won’t be able to customize the LCD screen or upload GIFs to it — it’s worth considering a keyboard like the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-rt100-pro-review">Epomaker RT75</a> ($91) instead.</p><p>Speaking of GIFs, uploading one to the Spectre 75 feels like an uphill climb, or like Sisyphus pushing a heavy boulder up said hill, only to watch it roll back down. The process seems simple enough at first. Navigate to the GIF tab, click on the + icon, and add your preferred GIF. When I did so, the software told me that the GIF had 61 frames and only 44 were allowed. You can delete specific frames within the software itself, but you can’t do so in bulk. You have to delete each frame individually — and this includes confirming <em>every time </em>that you’re certain you want to delete said frame.</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="tbriHkw2sqUbFGkCwBfYWm" name="Tecware_Spectre_75" alt="Tecware Spectre 75 keyboard's screen" src="https://cdn.mos.cms.futurecdn.net/tbriHkw2sqUbFGkCwBfYWm-1920-80.jpg" mos="" align="middle" fullscreen="1" width="2048" height="1152" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/tbriHkw2sqUbFGkCwBfYWm-1920-80.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: Tom's Guide)</span></figcaption></figure><p>It took some time, but I managed to trim the frames down to 44. I clicked ‘Upload to Keyboard’ and watched the progress bar fill up… and then the software crashed. I booted up the software again and repeated the process, only this time the software didn’t say that the frames were limited to 44 — strange, but at least it saves me a gazillion steps. Again, I clicked ‘Upload to Keyboard’ and this time, it worked (thankfully).</p><p>But that isn’t where my troubles stopped. I wanted to test the software’s macro recording feature but for some reason, none of the options in the macro tab were clickable. I reinstalled the software and the issue persisted. I even checked if the Spectre 75 had the latest firmware installed (it did).</p><p>The Spectre 75’s LCD screen is capable of displaying CPU and GPU stats, too, but the software needs to be running in the background — again, impossible to do on macOS. Overall, the software left a sour taste in my mouth, and it sticks out like a sore thumb.</p><h2 id="limited-stock-switch-choice">Limited stock switch choice</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tSoN6eKP7rSzQd8sqqY5v6" name="Tecware_Spectre75_Keyboard_012.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/tSoN6eKP7rSzQd8sqqY5v6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 comes with pre-lubed switches that are made in-house. Unfortunately, you’re limited to just two stock switch sets. You can choose either Cocoa Brown tactile or Candy Red linear switches. There are no clicky or silent switches, and you’ll have to buy your preferred ones separately.</p><p>Thankfully, the Spectre 75 is hot-swappable and compatible with south-facing 5-pin mechanical switches. It’s nice to see a budget keyboard offering hot-swappability when some pricey keyboards don’t (looking at you, Turtle Beach Command Series KB5). If you want more stock switch choice at checkout, I’d recommend the Keychron V1 8K.</p><h2 id="not-the-most-responsive-when-waking">Not the most responsive when waking</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bnVYxtLuhSJSPAodxiFxK7" name="Tecware_Spectre75_Keyboard_013.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/bnVYxtLuhSJSPAodxiFxK7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 last nail in the Tecware Spectre 75’s coffin is its tendency to not respond when it’s waking from sleep mode. In my testing, over Bluetooth and a wired connection, the keyboard wouldn’t respond after my laptop — and therefore, the Spectre 75 — had been asleep for a while. Though it didn’t happen <em>all </em>the time, I had to reconnect the keyboard a few times to get it working again. I tested this on macOS and Windows machines and had the same issue each time.</p><p>I haven’t had this issue with other mechanical keyboards I’ve tested. For that reason, and everything else I’ve mentioned up until now, I’d recommend the Keychron V1 8K or the Royal Kludge L75. I’ve tested both keyboards extensively and neither of them have given me this much grief.</p><p>I've reached out to Tecware's representative about this issue, to see whether it's limited to my review unit alone.</p><h2 class="article-body__section" id="section-tecware-spectre-75-review-verdict"><span>Tecware Spectre 75 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="hkNmhewXwLrRkkaVw6iww7" name="Tecware_Spectre75_Keyboard_005.JPG" alt="A black Tecware Spectre 75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/hkNmhewXwLrRkkaVw6iww7-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Tecware Spectre 75 is a decent mechanical keyboard for typing and casual gaming, thanks to its responsive linear switches and 1,000Hz polling rate. It features an LCD screen that you can view keyboard and system stats on, or display GIFs on, too.</p><p>But there isn’t much else to like about the Spectre 75. The companion software needed to customize the screen is janky and frustrating to use, while the keyboard itself sports a lackluster design and plasticky build. You’re limited to just two stock switch sets, too. I also struggled with the keyboard becoming unresponsive in my testing.</p><p>While $79 isn’t a high asking price, you can get better value for your money elsewhere. The Keychron V1 8K and the Royal Kludge L75 are fantastic keyboards under $80, and are much better choices if you want a feature-packed deck.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just tested the Turtle Beach Command Series KB5 — it’s a decent gaming keyboard but I’d love it more if it wasn’t for these flaws ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> have come leaps and bounds in recent years, and the Turtle Beach Command Series KB5 hopes to join the ranks. Packing highly responsive linear switches, 8,000Hz polling, and features like ReacTap in a sturdy, aluminum body, the KB5 is a formidable performer. It’s great to type on, too, and it features a handy touchscreen for on-the-go customization.</p><p>But is that enough to be considered one of the best in 2026, when the competition is so fierce? The KB5 is competent but its flaws are difficult to overlook when it costs nearly $150. It’s wired only; it isn’t hot-swappable; and its touchscreen can be sluggish. Of course, if none of those matter to you, and you’ve got the cash, the KB5 is still an okay choice for gaming and productivity.</p><p>For the complete breakdown, read my full Turtle Beach Command Series KB5 review.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-specs"><span>Turtle Beach Command Series KB5 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/Turtle-Beach-Full-Sized-Mechanical-Keyboard/dp/B0GWCW4GX2/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Mechanical-Touchscreen-Low-Profile/dp/B0GXZW5BX3/" target="_blank" rel="nofollow">£139</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Titan low-profile mechanical (linear)</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>100%, 109 keys + 2.4-inch touchscreen</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>1</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>18.78 x 10.12 x 1.06 inches (with palm rest)</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.43lbs (with palm rest)</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-turtle-beach-command-series-kb5-review-cheat-sheet"><span>Turtle Beach Command Series KB5 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A full-size mechanical keyboard with a customizable touchscreen</li><li><strong>Who is it for? </strong>Gamers and typists alike</li><li><strong>How much does it cost?</strong> The Turtle Beach Command Series KB5 is available for <a href="https://www.amazon.com/Turtle-Beach-Full-Sized-Mechanical-Keyboard/dp/B0GWCW4GX2/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Mechanical-Touchscreen-Low-Profile/dp/B0GXZW5BX3/" target="_blank" rel="nofollow">£139</a></li><li><strong>What do we like? </strong>The sturdy build, 8KHz polling for gaming, responsive switches, and customizable touchscreen</li><li><strong>What don’t we like? </strong>The wired-only and non-hot-swappable design, sometimes sluggish touchscreen, and hefty price tag</li></ul><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-the-ups"><span>Turtle Beach Command Series KB5 review: The ups</span></h2><p>The Turtle Beach Command Series KB5 is well-built and it’s a joy to type on. 8,000Hz polling and ReacTap make it great for gaming, and the customizable touchscreen lets you make changes on the fly.</p><h2 id="sturdy-construction">Sturdy construction</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rSnJhT63ahsCQHkYRkf3eZ" name="Turtlebeach_KB7_ 12.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/rSnJhT63ahsCQHkYRkf3eZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 KB5 is part of Turtle Beach’s Command Series, so it shares a lot of similarities with the <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb7-tkl-review">KB7 TKL</a> ($199) — one of them being the sturdy construction. Just like the KB7 TKL, the KB5 is made of an anodized aluminum top plate and a plastic chassis. There’s no flex in the body and no rattling. The aluminum plate gives the keyboard some heft too, as the KB5 weighs 2.43lbs (with the included palm rest). Though a little heavy, the KB5 is nowhere near as gargantuan as some of Keychron’s all-metal keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/keychron-q65-max-review">Q65 Max</a> (5.33lbs, $209) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q3-ultra-8k-review">Q3 Ultra 8K</a> (3.9lbs, $229).</p><p>As a full-size 100% keyboard, the KB5 has a large desk footprint too, measuring 18.78 x 10.12 x 1.06 inches (with palm rest), so it’s best suited to staying put at your desk. Given its length, the KB5 didn’t fit into my day bag. The keyboard’s wired-only nature may make you reluctant to travel with it 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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rQjzigPm2uYKVqLbAgdgkZ" name="Turtlebeach_KB7_ 8.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/rQjzigPm2uYKVqLbAgdgkZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Adding to the KB5’s appeal are its double-shot PBT keycaps which are more resistant to oil, wear and tear compared to their ABS counterparts. It’s nice to see Turtle Beach do away with the ABS keycaps its <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Vulcan II Max</a> ($229) uses — they get <em>very </em>shiny with fingerprints.</p><p>I also like the notched clickable volume barrel located right above the first four Function keys. You can use this to adjust the volume and mute playback (or remap it completely in Swarm II). Rolling its barrel is what I’d imagine the thrust lever on an airplane would feel like.</p><h2 id="excellent-for-typing">Excellent for typing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rMtEgDNLFJ48mcaM5AZTnZ" name="Turtlebeach_KB7_ 11.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/rMtEgDNLFJ48mcaM5AZTnZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though the Turtle Beach Command Series KB5 is primarily a gaming keyboard, it’s a sheer delight to type on. While its clacky, hollow sound profile is nothing to write home about, it still offers an enjoyable typing experience. Its Titan low-profile mechanical switches have a pre-travel distance of 1.2mm (total 3.1mm) which means that they don’t need to be pressed all the way down to activate. This helps you type faster, and even after a full day’s worth of typing, my fingers and hands didn’t feel fatigued.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Turtle Beach Command Series KB5</p></td><td  ><p>108</p></td><td  ><p>97%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/mchose-k99-v2-review">MCHOSE K99 V2</a></p></td><td  ><p>98</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">MCHOSE X75 V2</a></p></td><td  ><p>94</p></td><td  ><p>95.37%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100 Pro</a></p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.84</p></td><td  ><p>91.25%</p></td></tr></tbody></table></div><p>I put the KB5 through its paces by taking a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> as soon as I unboxed the keyboard. I wasn’t expecting to type as fast as I did, because the KB5 isn’t marketed as a productivity deck, and yet, I was able to achieve 108 words per minute with 97% accuracy. The keyboard upstaged all but one deck when it came to sheer speed: the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139). Color me impressed!</p><h2 id="8-000hz-polling-and-reactap">8,000Hz polling and ReacTap</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dzRYRKS2Z92qRVQpQ8YLkZ" name="Turtlebeach_KB7_ 10.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/dzRYRKS2Z92qRVQpQ8YLkZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB5’s biggest selling point is its 8,000Hz polling rate. Though impressive, 8KHz polling is overkill for 99% of gamers, and is an unnecessary drain on system resources. For most gamers, including myself, 1KHz polling is more than sufficient. You won’t notice latency issues or input lag regardless.</p><p>Still, I tested the KB5’s 8KHz polling by playing Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a>. As predicted, there were no latency issues to make a big song and dance about. All of my commands were promptly registered. I found the Titan low-profile linear switches extremely responsive too, and their 1.3mm pre-travel distance meant that I could lightly tap one key after another to execute a series of actions in quick succession.</p><p>You can also enable ReacTap by tapping the screen’s left edge and swiping left. ReacTap is Turtle Beach’s term for Last Key Prioritization (LKP), and it’s a type of Simultaneous Opposite Key Detection (SOCD) resolution. As you can probably tell by the name, it prioritizes the last key pressed, and I primarily use it for counter-strafing. Basically, if you hold down A and press D, the LKP resolution overrides A and instantly registers D (or vice versa). Doing so results in an immediate end to movement and therefore accuracy penalties in titles that have inertia in their movement mechanics, like Counter-Strike 2 (where LKP will get you kicked) and Valorant.</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:2010px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="AZBC6A5DarGhgSqUqpir2P" name="1000013631" alt="Turtle Beach Command Series KB5's touchscreen" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:88,l:269,cw:2010,ch:1131,q:80/AZBC6A5DarGhgSqUqpir2P.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" attribution="" endorsement="" class="inline"></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 KB5 features five dedicated macro keys too, located on the keyboard’s left-hand side. These can be configured to execute commands with just the tap of a button. You can use these to launch another program, record the last 30 seconds of gameplay, what-have-you. Four different commands can be bound to each key, and these layers can be accessed via key combinations, such as holding down Caps Lock / Easy Shift [+].</p><p>These macro keys are useful in both gaming and productivity, and you can assign work- or game-specific commands and save them to five customizable profiles.</p><h2 id="built-in-touchscreen-for-on-the-fly-customization">Built-in touchscreen for on-the-fly customization</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zocSEXEgoS7w5Zni4adkmZ" name="Turtlebeach_KB7_ 7.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/zocSEXEgoS7w5Zni4adkmZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>LCD and even OLED screens are becoming more and more common on keyboards. We’ve seen them on the likes of the <a href="https://www.tomsguide.com/computing/peripherals/razer-blackwidow-v4-pro-75-percent-review">Razer BlackWidow V4 Pro</a> ($189), the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-rt100-pro-review">Epomaker RT100 Pro</a> ($119), and now the Turtle Beach Command Series KB5. Unlike the large 4.3-inch touchscreen on the Turtle Beach Command Series KB7 TKL, the KB5’s 2.4-inch is much smaller but useful nonetheless.</p><p>The KB5’s touchscreen enables you to make adjustments to the keyboard’s settings on the fly and without having to launch Swarm II every time. You can use it to record macros, swap between five profiles, launch apps, manage audio playback, and more. Annoyingly, though, you can’t adjust the polling rate via the screen itself, and this can be done via Swarm II only.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/XsC3bU8BmS5RWJvcBGZYRe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/j9XZ2kvXev3839ZJacp8Ne-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/59J2G9nCvAH7nhXbeT3TLe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/GkW9Gpy674gcVyGWh7fYHe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/ffKyByfJkwg6ASzpkThXBe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/NjoUKHhs53EJJujth4AS5e-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure></figure><p>The touchscreen’s layout can be changed, too, and you’ll need to download the user-friendly Swarm II in order to do so. Via the Windows-only software (sorry, macOS users), you can add your preferred widgets to the screen, or select a preset template (productivity- or gaming-focused). If you play a lot of Counter-Strike, you might like the ‘KB5 CS’ preset, which displays stats like equipped weapon, bullets remaining, and total kills. </p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-the-downs"><span>Turtle Beach Command Series KB5 review: The downs</span></h2><p>The Turtle Beach Command Series KB5 isn’t hot-swappable, can’t be used wirelessly, and its touchscreen can be finicky at times, so is it worth the premium? I’m not convinced.</p><h2 id="sluggish-touchscreen">Sluggish touchscreen</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="oWjhbMyCZLbxnFkcUGrBkZ" name="Turtlebeach_KB7_ 5.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/oWjhbMyCZLbxnFkcUGrBkZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB5’s touchscreen is useful and behaves well for the most part, I found it sluggish at times. In my testing, I found that the touchscreen would randomly freeze and get stuck on one page, and I also noticed that my commands weren’t always registered. I had to tap a couple of times to get the touchscreen to skip the song I was listening to, for instance. Yes, even after updating the keyboard’s firmware, these issues didn’t magically disappear.</p><h2 id="wired-only">Wired-only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="t8fLDWp2nbVa8HjGWShMhZ" name="Turtlebeach_KB7_ 23.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/t8fLDWp2nbVa8HjGWShMhZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Sadly, the Turtle Beach Command Series KB5 is a wired-only keyboard so there’s no Bluetooth wireless connectivity. That also means the keyboard is designed to stay put at your desk. This isn’t necessarily a flaw but it’s disappointing given that so many mechanical keyboards today <em>are </em>wireless. This becomes an even bigger problem when you realize how expensive the KB5 is (more on that in a second).</p><p>If you want a wireless mechanical keyboard with 8,000Hz polling, I’d recommend the Keychron V1 Ultra 8K which features high polling over both wired and wireless connections. However, as I said earlier, 8,000Hz polling is overkill for most people anyway.</p><h2 id="not-hot-swappable">Not hot-swappable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2JvrthQmGg75okaBQwQDnZ" name="Turtlebeach_KB7_ 9.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/2JvrthQmGg75okaBQwQDnZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Okay, so the Turtle Beach Command Series KB5 is wired-only. Big deal. But my biggest gripe with the keyboard is that its switches aren’t hot-swappable. I find that surprising in 2026, when most keyboards (especially in this price range) <em>are </em>hot-swappable. Because the KB5 isn’t, you can’t change its switches by simply popping them out — you’ll need to de- and re-solder them to the PCB which is not only time-consuming, but it requires specialist tools and skill, too.</p><p>This means that if, say, one switch stops working, you’ll have to take the entire keyboard apart. I also feel like if you’re spending short of $150 on a keyboard, the least you should expect is hot-swappable switches. If this irks you as much as it does me, I’d again recommend the Keychron V1 Ultra 8K, the <a href="https://www.tomsguide.com/computing/peripherals/keychron-c1-pro-8k-review">Keychron C1 Pro 8K</a> ($54) or the Razer BlackWidow V4 Pro (which does enough to justify its $189 price tag).</p><h2 id="expensive-for-what-you-get">Expensive for what you get</h2><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="oWjhbMyCZLbxnFkcUGrBkZ" name="Turtlebeach_KB7_ 5.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:388,l:0,cw:960,ch:540,q:80/oWjhbMyCZLbxnFkcUGrBkZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 brings me to my last argument: the Turtle Beach Command Series KB5’s price tag. The keyboard retails for <a href="https://www.amazon.com/Turtle-Beach-Full-Sized-Mechanical-Keyboard/dp/B0GWCW4GX2/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Mechanical-Touchscreen-Low-Profile/dp/B0GXZW5BX3/" target="_blank" rel="nofollow">£139 at Amazon</a>, which is quite steep given that the keyboard is wired-only and isn’t hot-swappable. Its touchscreen, though useful, can be sluggish and is also heavily reliant on Swarm II, which isn’t available on macOS. </p><p>When brands like Epomaker and Keychron continue making powerful keyboards for a fraction of the KB5’s price, Turtle Beach’s offering is a hard sell. The Keychron V1 Ultra 8K, the Keychron C1 Pro 8K, and the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-p65-review">Epomaker P65</a> ($119) are much easier to recommend. Heck, even some outstanding Hall Effect decks, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> ($59), offer exceptional value for money while being cheaper than the KB5.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-verdict"><span>Turtle Beach Command Series KB5 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="a4NMzgStLdNkLLAYXFfLkY" name="Turtlebeach_KB7_ 4.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/a4NMzgStLdNkLLAYXFfLkY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB5 isn’t a <em>bad </em>keyboard — quite the opposite in fact. It boasts strong gaming performance thanks to features like 8,000Hz polling and ReacTap. It’s good for typing, too, and it’s fitted with a responsive touchscreen that lets you make adjustments on the go.</p><p>At the end of the day, though, are those features enough to justify the $150 asking price? The KB5, though well-built and capable, isn’t hot-swappable, and when its competitors are offering 8KHz polling over wireless connections, the KB5’s wired-only design leaves a lot to be desired. The sluggish touchscreen is frustrating at this price point, too. You’d be better off with the Keychron V1 8K, if it’s a bang-for-your-buck mechanical deck you’re after.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb5-review</link>
                                                                            <description>
                            <![CDATA[ The Turtle Beach Command Series KB5 is a good productivity and gaming keyboard with 8KHz polling and ReacTap, but at $150, its flaws are hard to overlook. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">prqiiujh3amBdnPzBb5Pbd</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/WVEc7wPxssdYAahxsPAJpZ-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 11 Sep 2026 12:08:15 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/WVEc7wPxssdYAahxsPAJpZ-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming]]></media:description>                                                            <media:text><![CDATA[A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming]]></media:text>
                                <media:title type="plain"><![CDATA[A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/WVEc7wPxssdYAahxsPAJpZ-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> have come leaps and bounds in recent years, and the Turtle Beach Command Series KB5 hopes to join the ranks. Packing highly responsive linear switches, 8,000Hz polling, and features like ReacTap in a sturdy, aluminum body, the KB5 is a formidable performer. It’s great to type on, too, and it features a handy touchscreen for on-the-go customization.</p><p>But is that enough to be considered one of the best in 2026, when the competition is so fierce? The KB5 is competent but its flaws are difficult to overlook when it costs nearly $150. It’s wired only; it isn’t hot-swappable; and its touchscreen can be sluggish. Of course, if none of those matter to you, and you’ve got the cash, the KB5 is still an okay choice for gaming and productivity.</p><p>For the complete breakdown, read my full Turtle Beach Command Series KB5 review.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-specs"><span>Turtle Beach Command Series KB5 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/Turtle-Beach-Full-Sized-Mechanical-Keyboard/dp/B0GWCW4GX2/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Mechanical-Touchscreen-Low-Profile/dp/B0GXZW5BX3/" target="_blank" rel="nofollow">£139</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Titan low-profile mechanical (linear)</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>100%, 109 keys + 2.4-inch touchscreen</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>1</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>18.78 x 10.12 x 1.06 inches (with palm rest)</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.43lbs (with palm rest)</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-turtle-beach-command-series-kb5-review-cheat-sheet"><span>Turtle Beach Command Series KB5 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A full-size mechanical keyboard with a customizable touchscreen</li><li><strong>Who is it for? </strong>Gamers and typists alike</li><li><strong>How much does it cost?</strong> The Turtle Beach Command Series KB5 is available for <a href="https://www.amazon.com/Turtle-Beach-Full-Sized-Mechanical-Keyboard/dp/B0GWCW4GX2/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Mechanical-Touchscreen-Low-Profile/dp/B0GXZW5BX3/" target="_blank" rel="nofollow">£139</a></li><li><strong>What do we like? </strong>The sturdy build, 8KHz polling for gaming, responsive switches, and customizable touchscreen</li><li><strong>What don’t we like? </strong>The wired-only and non-hot-swappable design, sometimes sluggish touchscreen, and hefty price tag</li></ul><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-the-ups"><span>Turtle Beach Command Series KB5 review: The ups</span></h2><p>The Turtle Beach Command Series KB5 is well-built and it’s a joy to type on. 8,000Hz polling and ReacTap make it great for gaming, and the customizable touchscreen lets you make changes on the fly.</p><h2 id="sturdy-construction">Sturdy construction</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rSnJhT63ahsCQHkYRkf3eZ" name="Turtlebeach_KB7_ 12.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/rSnJhT63ahsCQHkYRkf3eZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 KB5 is part of Turtle Beach’s Command Series, so it shares a lot of similarities with the <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb7-tkl-review">KB7 TKL</a> ($199) — one of them being the sturdy construction. Just like the KB7 TKL, the KB5 is made of an anodized aluminum top plate and a plastic chassis. There’s no flex in the body and no rattling. The aluminum plate gives the keyboard some heft too, as the KB5 weighs 2.43lbs (with the included palm rest). Though a little heavy, the KB5 is nowhere near as gargantuan as some of Keychron’s all-metal keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/keychron-q65-max-review">Q65 Max</a> (5.33lbs, $209) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q3-ultra-8k-review">Q3 Ultra 8K</a> (3.9lbs, $229).</p><p>As a full-size 100% keyboard, the KB5 has a large desk footprint too, measuring 18.78 x 10.12 x 1.06 inches (with palm rest), so it’s best suited to staying put at your desk. Given its length, the KB5 didn’t fit into my day bag. The keyboard’s wired-only nature may make you reluctant to travel with it 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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rQjzigPm2uYKVqLbAgdgkZ" name="Turtlebeach_KB7_ 8.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/rQjzigPm2uYKVqLbAgdgkZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Adding to the KB5’s appeal are its double-shot PBT keycaps which are more resistant to oil, wear and tear compared to their ABS counterparts. It’s nice to see Turtle Beach do away with the ABS keycaps its <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Vulcan II Max</a> ($229) uses — they get <em>very </em>shiny with fingerprints.</p><p>I also like the notched clickable volume barrel located right above the first four Function keys. You can use this to adjust the volume and mute playback (or remap it completely in Swarm II). Rolling its barrel is what I’d imagine the thrust lever on an airplane would feel like.</p><h2 id="excellent-for-typing">Excellent for typing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rMtEgDNLFJ48mcaM5AZTnZ" name="Turtlebeach_KB7_ 11.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/rMtEgDNLFJ48mcaM5AZTnZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though the Turtle Beach Command Series KB5 is primarily a gaming keyboard, it’s a sheer delight to type on. While its clacky, hollow sound profile is nothing to write home about, it still offers an enjoyable typing experience. Its Titan low-profile mechanical switches have a pre-travel distance of 1.2mm (total 3.1mm) which means that they don’t need to be pressed all the way down to activate. This helps you type faster, and even after a full day’s worth of typing, my fingers and hands didn’t feel fatigued.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Turtle Beach Command Series KB5</p></td><td  ><p>108</p></td><td  ><p>97%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/mchose-k99-v2-review">MCHOSE K99 V2</a></p></td><td  ><p>98</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">MCHOSE X75 V2</a></p></td><td  ><p>94</p></td><td  ><p>95.37%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100 Pro</a></p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.84</p></td><td  ><p>91.25%</p></td></tr></tbody></table></div><p>I put the KB5 through its paces by taking a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> as soon as I unboxed the keyboard. I wasn’t expecting to type as fast as I did, because the KB5 isn’t marketed as a productivity deck, and yet, I was able to achieve 108 words per minute with 97% accuracy. The keyboard upstaged all but one deck when it came to sheer speed: the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139). Color me impressed!</p><h2 id="8-000hz-polling-and-reactap">8,000Hz polling and ReacTap</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dzRYRKS2Z92qRVQpQ8YLkZ" name="Turtlebeach_KB7_ 10.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/dzRYRKS2Z92qRVQpQ8YLkZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB5’s biggest selling point is its 8,000Hz polling rate. Though impressive, 8KHz polling is overkill for 99% of gamers, and is an unnecessary drain on system resources. For most gamers, including myself, 1KHz polling is more than sufficient. You won’t notice latency issues or input lag regardless.</p><p>Still, I tested the KB5’s 8KHz polling by playing Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a>. As predicted, there were no latency issues to make a big song and dance about. All of my commands were promptly registered. I found the Titan low-profile linear switches extremely responsive too, and their 1.3mm pre-travel distance meant that I could lightly tap one key after another to execute a series of actions in quick succession.</p><p>You can also enable ReacTap by tapping the screen’s left edge and swiping left. ReacTap is Turtle Beach’s term for Last Key Prioritization (LKP), and it’s a type of Simultaneous Opposite Key Detection (SOCD) resolution. As you can probably tell by the name, it prioritizes the last key pressed, and I primarily use it for counter-strafing. Basically, if you hold down A and press D, the LKP resolution overrides A and instantly registers D (or vice versa). Doing so results in an immediate end to movement and therefore accuracy penalties in titles that have inertia in their movement mechanics, like Counter-Strike 2 (where LKP will get you kicked) and Valorant.</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:2010px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="AZBC6A5DarGhgSqUqpir2P" name="1000013631" alt="Turtle Beach Command Series KB5's touchscreen" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:88,l:269,cw:2010,ch:1131,q:80/AZBC6A5DarGhgSqUqpir2P.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" attribution="" endorsement="" class="inline"></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 KB5 features five dedicated macro keys too, located on the keyboard’s left-hand side. These can be configured to execute commands with just the tap of a button. You can use these to launch another program, record the last 30 seconds of gameplay, what-have-you. Four different commands can be bound to each key, and these layers can be accessed via key combinations, such as holding down Caps Lock / Easy Shift [+].</p><p>These macro keys are useful in both gaming and productivity, and you can assign work- or game-specific commands and save them to five customizable profiles.</p><h2 id="built-in-touchscreen-for-on-the-fly-customization">Built-in touchscreen for on-the-fly customization</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zocSEXEgoS7w5Zni4adkmZ" name="Turtlebeach_KB7_ 7.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/zocSEXEgoS7w5Zni4adkmZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>LCD and even OLED screens are becoming more and more common on keyboards. We’ve seen them on the likes of the <a href="https://www.tomsguide.com/computing/peripherals/razer-blackwidow-v4-pro-75-percent-review">Razer BlackWidow V4 Pro</a> ($189), the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-rt100-pro-review">Epomaker RT100 Pro</a> ($119), and now the Turtle Beach Command Series KB5. Unlike the large 4.3-inch touchscreen on the Turtle Beach Command Series KB7 TKL, the KB5’s 2.4-inch is much smaller but useful nonetheless.</p><p>The KB5’s touchscreen enables you to make adjustments to the keyboard’s settings on the fly and without having to launch Swarm II every time. You can use it to record macros, swap between five profiles, launch apps, manage audio playback, and more. Annoyingly, though, you can’t adjust the polling rate via the screen itself, and this can be done via Swarm II only.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/XsC3bU8BmS5RWJvcBGZYRe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/j9XZ2kvXev3839ZJacp8Ne-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/59J2G9nCvAH7nhXbeT3TLe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/GkW9Gpy674gcVyGWh7fYHe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/ffKyByfJkwg6ASzpkThXBe-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/NjoUKHhs53EJJujth4AS5e-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure></figure><p>The touchscreen’s layout can be changed, too, and you’ll need to download the user-friendly Swarm II in order to do so. Via the Windows-only software (sorry, macOS users), you can add your preferred widgets to the screen, or select a preset template (productivity- or gaming-focused). If you play a lot of Counter-Strike, you might like the ‘KB5 CS’ preset, which displays stats like equipped weapon, bullets remaining, and total kills. </p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-the-downs"><span>Turtle Beach Command Series KB5 review: The downs</span></h2><p>The Turtle Beach Command Series KB5 isn’t hot-swappable, can’t be used wirelessly, and its touchscreen can be finicky at times, so is it worth the premium? I’m not convinced.</p><h2 id="sluggish-touchscreen">Sluggish touchscreen</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="oWjhbMyCZLbxnFkcUGrBkZ" name="Turtlebeach_KB7_ 5.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/oWjhbMyCZLbxnFkcUGrBkZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB5’s touchscreen is useful and behaves well for the most part, I found it sluggish at times. In my testing, I found that the touchscreen would randomly freeze and get stuck on one page, and I also noticed that my commands weren’t always registered. I had to tap a couple of times to get the touchscreen to skip the song I was listening to, for instance. Yes, even after updating the keyboard’s firmware, these issues didn’t magically disappear.</p><h2 id="wired-only">Wired-only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="t8fLDWp2nbVa8HjGWShMhZ" name="Turtlebeach_KB7_ 23.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/t8fLDWp2nbVa8HjGWShMhZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Sadly, the Turtle Beach Command Series KB5 is a wired-only keyboard so there’s no Bluetooth wireless connectivity. That also means the keyboard is designed to stay put at your desk. This isn’t necessarily a flaw but it’s disappointing given that so many mechanical keyboards today <em>are </em>wireless. This becomes an even bigger problem when you realize how expensive the KB5 is (more on that in a second).</p><p>If you want a wireless mechanical keyboard with 8,000Hz polling, I’d recommend the Keychron V1 Ultra 8K which features high polling over both wired and wireless connections. However, as I said earlier, 8,000Hz polling is overkill for most people anyway.</p><h2 id="not-hot-swappable">Not hot-swappable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2JvrthQmGg75okaBQwQDnZ" name="Turtlebeach_KB7_ 9.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/2JvrthQmGg75okaBQwQDnZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Okay, so the Turtle Beach Command Series KB5 is wired-only. Big deal. But my biggest gripe with the keyboard is that its switches aren’t hot-swappable. I find that surprising in 2026, when most keyboards (especially in this price range) <em>are </em>hot-swappable. Because the KB5 isn’t, you can’t change its switches by simply popping them out — you’ll need to de- and re-solder them to the PCB which is not only time-consuming, but it requires specialist tools and skill, too.</p><p>This means that if, say, one switch stops working, you’ll have to take the entire keyboard apart. I also feel like if you’re spending short of $150 on a keyboard, the least you should expect is hot-swappable switches. If this irks you as much as it does me, I’d again recommend the Keychron V1 Ultra 8K, the <a href="https://www.tomsguide.com/computing/peripherals/keychron-c1-pro-8k-review">Keychron C1 Pro 8K</a> ($54) or the Razer BlackWidow V4 Pro (which does enough to justify its $189 price tag).</p><h2 id="expensive-for-what-you-get">Expensive for what you get</h2><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="oWjhbMyCZLbxnFkcUGrBkZ" name="Turtlebeach_KB7_ 5.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:388,l:0,cw:960,ch:540,q:80/oWjhbMyCZLbxnFkcUGrBkZ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 brings me to my last argument: the Turtle Beach Command Series KB5’s price tag. The keyboard retails for <a href="https://www.amazon.com/Turtle-Beach-Full-Sized-Mechanical-Keyboard/dp/B0GWCW4GX2/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Mechanical-Touchscreen-Low-Profile/dp/B0GXZW5BX3/" target="_blank" rel="nofollow">£139 at Amazon</a>, which is quite steep given that the keyboard is wired-only and isn’t hot-swappable. Its touchscreen, though useful, can be sluggish and is also heavily reliant on Swarm II, which isn’t available on macOS. </p><p>When brands like Epomaker and Keychron continue making powerful keyboards for a fraction of the KB5’s price, Turtle Beach’s offering is a hard sell. The Keychron V1 Ultra 8K, the Keychron C1 Pro 8K, and the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-p65-review">Epomaker P65</a> ($119) are much easier to recommend. Heck, even some outstanding Hall Effect decks, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> ($59), offer exceptional value for money while being cheaper than the KB5.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb5-review-verdict"><span>Turtle Beach Command Series KB5 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="a4NMzgStLdNkLLAYXFfLkY" name="Turtlebeach_KB7_ 4.JPG" alt="A Turtle Beach Command Series KB5 wired-only mechanical keyboard for gaming" src="https://cdn.mos.cms.futurecdn.net/a4NMzgStLdNkLLAYXFfLkY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB5 isn’t a <em>bad </em>keyboard — quite the opposite in fact. It boasts strong gaming performance thanks to features like 8,000Hz polling and ReacTap. It’s good for typing, too, and it’s fitted with a responsive touchscreen that lets you make adjustments on the go.</p><p>At the end of the day, though, are those features enough to justify the $150 asking price? The KB5, though well-built and capable, isn’t hot-swappable, and when its competitors are offering 8KHz polling over wireless connections, the KB5’s wired-only design leaves a lot to be desired. The sluggish touchscreen is frustrating at this price point, too. You’d be better off with the Keychron V1 8K, if it’s a bang-for-your-buck mechanical deck you’re after.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I’ve been using MelGeek’s Made68 Ultra V2 for three weeks and I’m in love with its magnetic switches, slick styling and clean sound ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’ve been through a handful of magnetic‑switch boards over the last few months, and in that time I’ve figured out what I actually want from them — precise actuation, a clean, non‑ringy acoustic, and a weighty, stable case that keeps the sound consistent. MelGeek’s Made68 Ultra V2 is the closest to that sweet spot I’ve tested so far: the magnetic action hits the right balance of smooth and snappy, the chassis keeps resonance in check, and the overall sound leans towards deep and satisfying rather than bright or clacky.</p><p>It also looks the business, with a simple yet stylish vibe that’s undeniably aimed at gamers — its translucent keycaps and personalised lighting are testaments to that — but still manages to bring a level of refinement and precision that’d let it slip onto the desk of the average ultra‑modern professional without looking too out of place.</p><p>As someone who hardly ever uses a numpad, 75% keyboards are typically my favourite layout. However, the Made68 Ultra V2’s 65% layout doesn’t feel compromised to me — it’s compact without feeling cramped, and every key is within easy reach for smooth, accurate typing. </p><p>As for gaming, the Ultra V2 delivers all the high end features you’d expect from a keyboard with Hall effect switches, along with some sweet AI-powered lighting control options that give the board a personality all of its own.</p><p>While I can think of a couple of small additions that would make it the perfect keyboard for me, I could easily see myself using the Made68 Ultra V2 as my daily driver for years to come — here’s why.</p><h2 id="it-sounds-fantastic-and-feels-remarkably-precise">It sounds fantastic and feels remarkably precise</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FLbrBrk3kWBrcsPvbxQJhY" name="20260908_142342" alt="MelGeek Made68 Ultra V2 close-up on keys" src="https://cdn.mos.cms.futurecdn.net/FLbrBrk3kWBrcsPvbxQJhY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Made68 Ultra V2 I tested arrived with magnetic KOM Lite switches pre-installed, which lend the board a slightly lighter, more relaxed character. They land in a good middle-ground between creamy and snappy, leaning more towards a brighter sound than a deep thick one. The magnetic action feels smooth and predictable, with switches that provide a consistent, uniform press across the layout. That said, there’s no plasticky clack — just a clean, crispy sound that’s easy to settle into. </p><p>A big part of that comes down to MelGeek’s 6‑way silicone dampening and the use of a silicone gasket mount. I prefer it to the “foam everywhere” approach that some keyboards employ — the board feels cushioned without being mushy, and the silicone layers do a great job of minimising resonance. You get a firm, planted bottom‑out rather than a soft bounce, and the sound stays tight even when you’re typing furiously.</p><p>Compared to some of the creamier magnetic boards out there, the Made68 Ultra V2 is more focused on a clean, consistent feel. I recently tested <a href="https://www.tomsguide.com/computing/keyboards/i-almost-found-my-dream-keyboard-in-the-gravastar-mercury-v60-pro-if-it-werent-for-three-finicky-details">GravaStar’s Mercury V60 Pro</a>, and while I did prefer that keyboard’s deeper, chocolatey thock, the Ultra V2’s calmer, more controlled tone is admittedly easier to live with day‑to‑day — and far less distracting to those around you.</p><h2 id="exceptional-gaming-performance">Exceptional gaming 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="wrxJ7oeVvbe6LX5AWmkVbe" name="20260908_143120" alt="MelGeek Made68 Ultra V2 with some keycaps and a switch removed" src="https://cdn.mos.cms.futurecdn.net/wrxJ7oeVvbe6LX5AWmkVbe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 great as the Made68 Ultra V2 is for typing, it truly soars for gaming. Its magnetic switches provide adjustable actuation and rapid trigger functionality as expected, and you can really dial them into your ideal settings using MelGeek’s browser-based Hive 2.0 software. You can drop actuation right down to 0.1mm for quick reactions or push it higher if you prefer a firmer, more deliberate press. The board tracks movement with 0.01mm precision, so even tiny lifts or taps register straight away.</p><p>Rapid Trigger is just as sharp. Keys reset the moment you lift them, even slightly, thanks to an adjustable RT range that goes all the way down to 0.01mm. At those extreme low settings it can feel a bit too sensitive — you’ll occasionally fire an input just by brushing a key or shifting your hand. Thankfully, you can tailor it to your liking — in my experience, bumping actuation or RT up even a fraction gives you a more controlled feel without losing the speed that makes magnetic switches great for fast‑paced games.</p><p>Latency is properly low, with 8,000Hz polling, a 16,000Hz scan rate, and a listed 0.125ms response time. In practice, the board keeps up whether you’re flicking through menus or right in the thick of a battle, instantly responding to your inputs in a clean, predictable manner.</p><h2 id="ai-powered-lighting-and-personalisation">AI-powered lighting and personalisation</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ktw8RTc4RngUgHAdVGJqXk" name="20260908_142633" alt="MelGeek Made68 Ultra V2 rear view showing light bar and mode button" src="https://cdn.mos.cms.futurecdn.net/ktw8RTc4RngUgHAdVGJqXk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 take personalisation even further with the hardware itself. The Ultra V2’s light bar can be customised to match whatever theme you’re running, though its purple tint does prevent certain colours from showing as intended. MelGeek also sells 3D‑printed light guide panels, which (in theory) let you shape how the RGB spreads across the sides of the case, as well as swappable side panels which can be easily screwed on, though both products have been sold out on MelGeek’s website for as long as I’ve been testing the Ultra V2.</p><p>The board’s AI‑powered lighting layer sits over the top of the usual RGB controls, adding a bit of smarts without turning the whole thing into a light show. Instead of cycling through random effects, it reacts to what you’re doing and keeps the lighting feeling relevant rather than noisy. In practice, it tends to make small, practical adjustments as you move between tasks or games, giving the keyboard a sense of personality without demanding constant tinkering.</p><p>Hive 2.0 gives you solid control — per‑key colours, simple dynamic effects, macros, profiles and all the usual tuning options are there, even if the lighting system itself is still pretty limited compared to what the hardware could do. You can set up game‑specific profiles (and cycle through them using a physical ‘mode’ button underneath the lightbar on the right side), adjust brightness curves or just keep things simple with a single clean theme, and once you’ve saved a profile the board switches between them quickly and reliably. If you want more traditional control of the Ultra V2’s RGB, Hive 2.0 handles that too, though it’s a bit more basic than competing software, with only per‑key static lighting and no support for layered or multi‑zone effects.</p><p>Real‑Time Game Sync (VCT) is the arguably the flashiest part of the package, but it’s handled with a light touch. Instead of blasting the whole board with rainbow effects, it reacts to in‑game events, like health changes or the use of skills and abilities, making your game feel more immersive without pulling your eyes off the screen. </p><h2 id="two-small-misses">Two small misses</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="6eEmfDUMqgw9n2rhaBEeuK" name="20260908_152448" alt="MelGeek Made68 Ultra V2 side view showing side glow and side panels" src="https://cdn.mos.cms.futurecdn.net/6eEmfDUMqgw9n2rhaBEeuK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>On a board that otherwise nails fundamentals, there are two design choices that stand out as avoidable compromises. First, there’s no dedicated volume knob or rocker. Once you’ve used a physical control, its absence sticks out straight away. Sure, you can bump the volume up or down by holding Fn and tapping the arrow keys, but I can’t imagine anyone on this blue planet choosing that over a simple knob — it’s one of those small conveniences that matters more than you expect, especially when you need to drop the volume in a hurry.</p><p>Second, the stock transparent keycaps are a mixed bag. They look distinctive, but they don’t diffuse RGB softly — the LEDs appear more direct and clinical than the warm glow you get with thicker, opaque PBT caps. On top of that, the transparency and dark purple text can make the keys harder to read if you do ever need to look down. And while I love how the Ultra V2 sounds, the clear caps don’t help the board reach its full potential — I haven’t had a chance to test it myself, but I bet that swapping to a denser PBT set will deepen and tighten the sound even further than it already is.</p><p>The Made68 Ultra V2 is a refined, well‑built keyboard that prioritises consistent feel, controlled acoustics and high-level gaming precision. It may not be the loudest or most characterful option, but it has consistently delivered one of the most comfortable and dependable typing experiences I’ve had recently. If you value typing comfort, stability and a mature build, it’s worth considering — just be prepared to use shortcuts for volume control or different keycaps if those two omissions matter to you.</p><div data-widget-type="productname" data-model-name="MelGeek Made68 Ultra V2"></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/computing/peripherals/mechanical-keyboards-are-dead-heres-why-you-should-only-buy-a-magnetic-keyboard-for-gaming">Here’s why you should only buy a magnetic keyboard for gaming</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-almost-found-my-dream-keyboard-in-the-gravastar-mercury-v60-pro-if-it-werent-for-three-finicky-details">The GravaStar Mercury V60 Pro is almost perfect except for three finicky details</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/corsairs-vanguard-pro-96-keyboard-has-convinced-me-that-magnetic-switches-arent-just-hype">Corsair’s Vanguard Pro 96 keyboard has convinced me that magnetic switches aren’t just hype</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/ive-been-using-melgeeks-made68-ultra-v2-for-three-weeks-and-im-in-love-with-its-magnetic-switches-slick-styling-and-clean-sound</link>
                                                                            <description>
                            <![CDATA[ MelGeek's Made68 Ultra V2 magnetic keyboard is a dream for both gaming and typing — here are my main takeaways after three weeks of continuous use. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">jtRxw55bSN3zTkCf7uVjd3</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/2QVt8nHmvqrUer5NCgsQDK-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 08 Sep 2026 06:21:54 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                                                                                    <dc:creator><![CDATA[ Stephen Lambrechts ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/JN3jEQpqyyt32as8AJJqXe-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Stephen Lambrechts is the Managing Editor of Tom&#039;s Guide AU and has written professionally across the categories of tech, film, television and gaming for the last 15 years. Before Tom&#039;s Guide, he spent several years as a Senior Journalist at TechRadar, had a brief stint as Editor in Chief at Official Xbox Magazine Australia, and worked at IGN. He&#039;s also written for such mags and sites as APC, TechLife Australia, T3, FilmInk, Daily Telegraph and AskMen. Additionally, he&#039;s a Tomatometer-approved critic on Rotten Tomatoes, and has offered his tech advice on radio and television.&lt;/p&gt;&lt;p&gt;He&#039;s an expert when it comes to smartphones, TVs, gaming and streaming, having contributed many reviews in each of those categories over the years. Despite his many articles on streaming, he&#039;s a passionate collector of physical media who enjoys listening to vinyl records and acquiring obscure horror movies from the &#039;60s, &#039;70s and 80s. He can usually be seen rocking the latest retro sneaker releases, particularly Air Jordans and Reebok Pumps. Occasionally, he also indulges in other non-hipster stuff, like hiking.&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/2QVt8nHmvqrUer5NCgsQDK-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[MelGeek Made68 Ultra V2 keyboard on desk]]></media:description>                                                            <media:text><![CDATA[MelGeek Made68 Ultra V2 keyboard on desk]]></media:text>
                                <media:title type="plain"><![CDATA[MelGeek Made68 Ultra V2 keyboard on desk]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/2QVt8nHmvqrUer5NCgsQDK-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’ve been through a handful of magnetic‑switch boards over the last few months, and in that time I’ve figured out what I actually want from them — precise actuation, a clean, non‑ringy acoustic, and a weighty, stable case that keeps the sound consistent. MelGeek’s Made68 Ultra V2 is the closest to that sweet spot I’ve tested so far: the magnetic action hits the right balance of smooth and snappy, the chassis keeps resonance in check, and the overall sound leans towards deep and satisfying rather than bright or clacky.</p><p>It also looks the business, with a simple yet stylish vibe that’s undeniably aimed at gamers — its translucent keycaps and personalised lighting are testaments to that — but still manages to bring a level of refinement and precision that’d let it slip onto the desk of the average ultra‑modern professional without looking too out of place.</p><p>As someone who hardly ever uses a numpad, 75% keyboards are typically my favourite layout. However, the Made68 Ultra V2’s 65% layout doesn’t feel compromised to me — it’s compact without feeling cramped, and every key is within easy reach for smooth, accurate typing. </p><p>As for gaming, the Ultra V2 delivers all the high end features you’d expect from a keyboard with Hall effect switches, along with some sweet AI-powered lighting control options that give the board a personality all of its own.</p><p>While I can think of a couple of small additions that would make it the perfect keyboard for me, I could easily see myself using the Made68 Ultra V2 as my daily driver for years to come — here’s why.</p><h2 id="it-sounds-fantastic-and-feels-remarkably-precise">It sounds fantastic and feels remarkably precise</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FLbrBrk3kWBrcsPvbxQJhY" name="20260908_142342" alt="MelGeek Made68 Ultra V2 close-up on keys" src="https://cdn.mos.cms.futurecdn.net/FLbrBrk3kWBrcsPvbxQJhY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Made68 Ultra V2 I tested arrived with magnetic KOM Lite switches pre-installed, which lend the board a slightly lighter, more relaxed character. They land in a good middle-ground between creamy and snappy, leaning more towards a brighter sound than a deep thick one. The magnetic action feels smooth and predictable, with switches that provide a consistent, uniform press across the layout. That said, there’s no plasticky clack — just a clean, crispy sound that’s easy to settle into. </p><p>A big part of that comes down to MelGeek’s 6‑way silicone dampening and the use of a silicone gasket mount. I prefer it to the “foam everywhere” approach that some keyboards employ — the board feels cushioned without being mushy, and the silicone layers do a great job of minimising resonance. You get a firm, planted bottom‑out rather than a soft bounce, and the sound stays tight even when you’re typing furiously.</p><p>Compared to some of the creamier magnetic boards out there, the Made68 Ultra V2 is more focused on a clean, consistent feel. I recently tested <a href="https://www.tomsguide.com/computing/keyboards/i-almost-found-my-dream-keyboard-in-the-gravastar-mercury-v60-pro-if-it-werent-for-three-finicky-details">GravaStar’s Mercury V60 Pro</a>, and while I did prefer that keyboard’s deeper, chocolatey thock, the Ultra V2’s calmer, more controlled tone is admittedly easier to live with day‑to‑day — and far less distracting to those around you.</p><h2 id="exceptional-gaming-performance">Exceptional gaming 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="wrxJ7oeVvbe6LX5AWmkVbe" name="20260908_143120" alt="MelGeek Made68 Ultra V2 with some keycaps and a switch removed" src="https://cdn.mos.cms.futurecdn.net/wrxJ7oeVvbe6LX5AWmkVbe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 great as the Made68 Ultra V2 is for typing, it truly soars for gaming. Its magnetic switches provide adjustable actuation and rapid trigger functionality as expected, and you can really dial them into your ideal settings using MelGeek’s browser-based Hive 2.0 software. You can drop actuation right down to 0.1mm for quick reactions or push it higher if you prefer a firmer, more deliberate press. The board tracks movement with 0.01mm precision, so even tiny lifts or taps register straight away.</p><p>Rapid Trigger is just as sharp. Keys reset the moment you lift them, even slightly, thanks to an adjustable RT range that goes all the way down to 0.01mm. At those extreme low settings it can feel a bit too sensitive — you’ll occasionally fire an input just by brushing a key or shifting your hand. Thankfully, you can tailor it to your liking — in my experience, bumping actuation or RT up even a fraction gives you a more controlled feel without losing the speed that makes magnetic switches great for fast‑paced games.</p><p>Latency is properly low, with 8,000Hz polling, a 16,000Hz scan rate, and a listed 0.125ms response time. In practice, the board keeps up whether you’re flicking through menus or right in the thick of a battle, instantly responding to your inputs in a clean, predictable manner.</p><h2 id="ai-powered-lighting-and-personalisation">AI-powered lighting and personalisation</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ktw8RTc4RngUgHAdVGJqXk" name="20260908_142633" alt="MelGeek Made68 Ultra V2 rear view showing light bar and mode button" src="https://cdn.mos.cms.futurecdn.net/ktw8RTc4RngUgHAdVGJqXk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 take personalisation even further with the hardware itself. The Ultra V2’s light bar can be customised to match whatever theme you’re running, though its purple tint does prevent certain colours from showing as intended. MelGeek also sells 3D‑printed light guide panels, which (in theory) let you shape how the RGB spreads across the sides of the case, as well as swappable side panels which can be easily screwed on, though both products have been sold out on MelGeek’s website for as long as I’ve been testing the Ultra V2.</p><p>The board’s AI‑powered lighting layer sits over the top of the usual RGB controls, adding a bit of smarts without turning the whole thing into a light show. Instead of cycling through random effects, it reacts to what you’re doing and keeps the lighting feeling relevant rather than noisy. In practice, it tends to make small, practical adjustments as you move between tasks or games, giving the keyboard a sense of personality without demanding constant tinkering.</p><p>Hive 2.0 gives you solid control — per‑key colours, simple dynamic effects, macros, profiles and all the usual tuning options are there, even if the lighting system itself is still pretty limited compared to what the hardware could do. You can set up game‑specific profiles (and cycle through them using a physical ‘mode’ button underneath the lightbar on the right side), adjust brightness curves or just keep things simple with a single clean theme, and once you’ve saved a profile the board switches between them quickly and reliably. If you want more traditional control of the Ultra V2’s RGB, Hive 2.0 handles that too, though it’s a bit more basic than competing software, with only per‑key static lighting and no support for layered or multi‑zone effects.</p><p>Real‑Time Game Sync (VCT) is the arguably the flashiest part of the package, but it’s handled with a light touch. Instead of blasting the whole board with rainbow effects, it reacts to in‑game events, like health changes or the use of skills and abilities, making your game feel more immersive without pulling your eyes off the screen. </p><h2 id="two-small-misses">Two small misses</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="6eEmfDUMqgw9n2rhaBEeuK" name="20260908_152448" alt="MelGeek Made68 Ultra V2 side view showing side glow and side panels" src="https://cdn.mos.cms.futurecdn.net/6eEmfDUMqgw9n2rhaBEeuK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>On a board that otherwise nails fundamentals, there are two design choices that stand out as avoidable compromises. First, there’s no dedicated volume knob or rocker. Once you’ve used a physical control, its absence sticks out straight away. Sure, you can bump the volume up or down by holding Fn and tapping the arrow keys, but I can’t imagine anyone on this blue planet choosing that over a simple knob — it’s one of those small conveniences that matters more than you expect, especially when you need to drop the volume in a hurry.</p><p>Second, the stock transparent keycaps are a mixed bag. They look distinctive, but they don’t diffuse RGB softly — the LEDs appear more direct and clinical than the warm glow you get with thicker, opaque PBT caps. On top of that, the transparency and dark purple text can make the keys harder to read if you do ever need to look down. And while I love how the Ultra V2 sounds, the clear caps don’t help the board reach its full potential — I haven’t had a chance to test it myself, but I bet that swapping to a denser PBT set will deepen and tighten the sound even further than it already is.</p><p>The Made68 Ultra V2 is a refined, well‑built keyboard that prioritises consistent feel, controlled acoustics and high-level gaming precision. It may not be the loudest or most characterful option, but it has consistently delivered one of the most comfortable and dependable typing experiences I’ve had recently. If you value typing comfort, stability and a mature build, it’s worth considering — just be prepared to use shortcuts for volume control or different keycaps if those two omissions matter to you.</p><div data-widget-type="productname" data-model-name="MelGeek Made68 Ultra V2"></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/computing/peripherals/mechanical-keyboards-are-dead-heres-why-you-should-only-buy-a-magnetic-keyboard-for-gaming">Here’s why you should only buy a magnetic keyboard for gaming</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-almost-found-my-dream-keyboard-in-the-gravastar-mercury-v60-pro-if-it-werent-for-three-finicky-details">The GravaStar Mercury V60 Pro is almost perfect except for three finicky details</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/corsairs-vanguard-pro-96-keyboard-has-convinced-me-that-magnetic-switches-arent-just-hype">Corsair’s Vanguard Pro 96 keyboard has convinced me that magnetic switches aren’t just hype</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ This $349 gaming headset gives SteelSeries, Razer and Audeze a serious run for their money ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Stealth Pro II is the flagship gaming headset from Turtle Beach, intended to compete with the heavy hitters from Razer, SteelSeries and Audeze. The <em>crème de la crème</em> are getting pricey now, with all the manufacturers above asking several hundred dollars for their very <a href="https://www.tomsguide.com/best-picks/best-gaming-headsets">best gaming headsets</a>. </p><p>The Stealth Pro II is no different, and hella pricey by objective standards, but actually comes in a little cheaper than some of its competition. It also offers excellent sound; an attractive, premium design; zero downtime battery; plus customization via a solid companion app. So could this be a fantastic balanced alternative to the main ultra-premium players?</p><p>As long as you don’t mind fairly weak ANC and a slightly tight clamp force! Find out more in my full Turtle Beach Stealth Pro II review.</p><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-cheat-sheet"><span>Turtle Beach Stealth Pro II review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A premium flagship gaming headset from Turtle Beach</li><li><strong>Who is it for? </strong>Competitive gamers and those who value sound quality</li><li><strong>What does it cost? </strong><a href="https://www.amazon.com/Turtle-Beach-Stealth-Premium-Wireless/dp/B0GN4NBD2V/" target="_blank" rel="nofollow">$349</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Stealth-Wireless-Multiplatform-Black/dp/B0GQCNJCPR" target="_blank" rel="nofollow">£299</a></li><li><strong>What do we like?</strong> The sound, mic, companion app, styling and build, base station and zero downtime battery system</li><li><strong>What don’t we like?</strong> The ANC is a little weak, and I found the headset a tad clampy</li></ul><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-specs"><span>Turtle Beach Stealth Pro II 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/Turtle-Beach-Stealth-Premium-Wireless/dp/B0GN4NBD2V/" target="_blank" rel="nofollow">$349</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Stealth-Wireless-Multiplatform-Black/dp/B0GQCNJCPR" target="_blank" rel="nofollow">£299</a></p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, white</p></td></tr><tr><td class="firstcol " ><p><strong>Platforms</strong></p></td><td  ><p>PC, Xbox, PlayStation, Bluetooth devices</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 3x 2.4Ghz, 1x Bluetooth</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>2x rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>40 hours combined</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>10-40,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Drivers</strong></p></td><td  ><p>60mm coaxial</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint connectivity</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>4 (3x 2.4G, 1x BT)</p></td></tr><tr><td class="firstcol " ><p><strong>Microphone</strong></p></td><td  ><p>Unidirectional floating boom mic</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>13.8 ounces / 393 grams</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-the-ups"><span>Turtle Beach Stealth Pro II review: The ups</span></h2><p>There’s a lot to love about the Stealth Pro II. Sound quality is excellent, there’s a useful companion app, a super clear mic, and a handy base station for ‘zero downtime’ battery life. It’s also moderately priced versus competition.</p><h2 id="excellent-sound-quality">Excellent 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:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HFWdi2AigrzMKFpu3MX5bc" name="Turtle Beach Stealth Pro II_test shots2" alt="The Turtle Beach Stealth Pro II with an earcup removed showing the coaxial driver" src="https://cdn.mos.cms.futurecdn.net/HFWdi2AigrzMKFpu3MX5bc-1920-80.jpg" mos="" align="middle" fullscreen="1" width="2560" height="1440" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/HFWdi2AigrzMKFpu3MX5bc-1920-80.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">Coaxial drivers feature a tweeter that sits in the middle/on top of a main woofer. The tweeter sits under that central circular housing, while the main woofer is the larger, fluted circle. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Turtle Beach Stealth Pro II’s biggest strength is its sound quality. It uses a coaxial driver setup, with a 60mm woofer and separate tweeter in each earcup. This, in theory, gives better clarity and more separation between higher and lower frequencies, as each driver can focus on its own job. Connectivity-wise, there’s 24-bit/96kHz high resolution data/sampling rates over 2.4G, so this is a true hi-res headset, akin to the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/steelseries-arctis-nova-pro-omni-review">SteelSeries Arctis Nova Pro Omni</a> / Elite and <a href="https://www.tomsguide.com/gaming/gaming-peripherals/audeze-maxwell-2-anc-review">Audeze Maxwell 2 ANC</a>.</p><p>The sound is super spacious with exceptional clarity up top and very strong imaging (this lets you pinpoint noises to a specific point). This naturally benefits competitive gameplay, letting me hear the location of enemy cues like footsteps or reloads in my test FPS games: Counter Strike 2 and Isonzo. It was also handy for stealthy games like Sniper Elite: Resistance, where I could again hear and locate even the quietest tapping of enemy footsteps. And it was ideal for immersion, giving three-dimensionality to sounds and effects in the campaign and battles of Total War: Rome II.</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="mXPzdXE3NLgxLtw9ZvfbHc" name="Turtle Beach Stealth Pro II_screenshots3" alt="A screenshot of the Turtle Beach Swarm II app being used to customize the Stealth Pro II" src="https://cdn.mos.cms.futurecdn.net/mXPzdXE3NLgxLtw9ZvfbHc-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/mXPzdXE3NLgxLtw9ZvfbHc-1920-80.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">Via the Swarm II app, you have access to a preset Footsteps EQ for a competitive edge. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Turtle Beach / Future)</span></figcaption></figure><p>There’s also Dolby Atmos (DA) support, so you can benefit from that codec in games that use it for surround sound. But even without DA, the sound is impressively spatial.</p><p>Tonal separation and mid-range clarity are really excellent, too. Each of the instruments and layers of the beautiful Rome II soundtrack sounded isolated yet cohesive — the panpipes, the atmospheric flutes, the drums, the horns, all occupied their own distinct space.</p><p>And thanks to that hefty 60mm main driver, there’s plenty of bass performance, giving strong, impactful artillery explosions in Isonzo. Bass response is not quite as nicely controlled as on the Arctis Nova Pro Omni or Elite, and you don’t get anywhere near the tightness of the Maxwell 2’s low-end. I noticed a little bit of booming and muddying at times, too, but it was nothing major, and generally, I loved listening to games and music on the Stealth Pro II.</p><p><em>Listen along to my audio testing playlist on </em><a href="https://www.tomsguide.com/audio/qobuz-review"><em>Qobuz</em></a><em>, our favorite </em><a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services"><em>music streaming service</em></a><em> for audiophiles.</em></p><iframe allow="" height="390" width="400" id="" style="" class="position-center" data-lazy-priority="high" data-lazy-src="https://widget.qobuz.com/playlist/59666746?zone=GB-en"></iframe><h2 id="useful-companion-app">Useful companion app</h2><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="WKivUJJSayR32JEJFmsJPc" name="Turtle Beach Stealth Pro II_screenshots4" alt="A screenshot of the Turtle Beach Swarm II app being used to customize the Stealth Pro II EQ" src="https://cdn.mos.cms.futurecdn.net/WKivUJJSayR32JEJFmsJPc-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/WKivUJJSayR32JEJFmsJPc-1920-80.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: Turtle Beach / Future)</span></figcaption></figure><p>Complementing the Stealth Pro II’s sound is an equally strong companion app, which gives you access to a full custom EQ, as well as EQ presets and a (<em>cringe</em>) “Superhuman hearing” mode — essentially some EQ presets to enhance things like footsteps.</p><p>There’s not the same enormous range of presets as on the SteelSeries GG app, with its hundreds-strong library of per-game EQs. But there’s still more than enough control in Swarm II to get the most out of your game audio.</p><p>In the app, you can also control the microphone EQ, update the headset and (my favorite aspect) change the control assignments to customize how the headset operates. So useful!</p><h2 id="multipoint-connectivity">Multipoint 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:1181px;"><p class="vanilla-image-block" style="padding-top:56.31%;"><img id="phhk8xiqF6fCKSfmxYZ5vK" name="Turtlebeach_StealthPro_II_012.JPG" alt="The Turtle Beach Stealth Pro II earcup and controls" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:380,l:413,cw:1181,ch:665,q:80/phhk8xiqF6fCKSfmxYZ5vK.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II offers multipoint connectivity. You can hook up two devices over 2.4G out of the box, using the base station and included USB dongle. You can also buy more dongles (the carry case even comes with a spare dongle slot) to connect to even more, up to four devices. </p><p>If that weren’t enough, you can also connect over Bluetooth simultaneously. I found this a little annoying, though, as the headset doesn’t mute game sounds to prioritize Bluetooth if, for example, you get a call or need to take a work meeting while gaming (which, as you can imagine, happens a lot in my job). You just have both going on at once, and the sidetone dial didn't work to fix this. It’s not a huge issue, but I like that the Nova Pro Omni and Maxwell 2 prioritize Bluetooth in those scenarios.</p><h2 id="super-clear-mic">Super clear mic</h2><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="3EgUuzZLcuowP7EX98QduK" name="Turtlebeach_StealthPro_II_014.JPG" alt="The Turtle Beach Stealth Pro II next to its base station and 2.4G dongle" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:540,l:476,cw:960,ch:540,q:80/3EgUuzZLcuowP7EX98QduK.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II uses a unidirectional mic that’s “floating,” meaning it’s suspended on rubber spokes to eliminate vibration. Turtle Beach says this gives exceptional clarity, although in fairness it’s just a great mic anyway, floating or otherwise.</p><p>Headset mics, even flagship ones, usually disappoint, so I was very surprised at the cleanness and airiness of voice recordings. There was arguably a little too much clarity, as I started to pick up on some of my inherent lispy and sibilant qualities — but there’s no denying how clean and noise-free the recordings were, especially using the ‘clarity’ mic EQ. </p><p>There’s environmental noise reduction, but being a unidirectional mic, that isn’t really needed — the mic only picks up sound from your mouth anyway. I recorded test clips with my window open to a very busy and loud road. Even with noise reduction off, I couldn’t hear the road in any recordings.</p><h2 id="stealthy-grown-up-design">Stealthy, grown-up 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="9im4tknZx6qrX7paMavoqK" name="Turtlebeach_StealthPro_II_011.JPG" alt="The Turtle Beach Stealth Pro II earcup showing the controls" src="https://cdn.mos.cms.futurecdn.net/9im4tknZx6qrX7paMavoqK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Turtle Beach gear is very often styled a little ‘gamer-y’ for my tastes. Even the pricey stuff is typically a bit garish for me, like the $200 <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb7-tkl-review">Turtle Beach Command KB7</a> we tested before this headset. So I was pleasantly surprised by how grown up the Stealth Pro II feels. The eponymous, stealthy, blacked-out look means it won’t seem out of place in any setup — whether you’re an RGB-clad unicorn puke kinda person, or an anti-RGBer like me.</p><p>I definitely prefer the look of the Stealth Pro II to the chunky Maxwell 2 ANC, and I’d say it edges the Arctis Nova Pro Omni and Elite (although there’s not much in it). For sheer prettiness, the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/razer-blackshark-v3-pro-review">Razer BlackShark V3 Pro</a> and <a href="https://www.tomsguide.com/gaming/gaming-peripherals/fractal-design-scape-review">Fractal Design Scape</a> still win out for me, but naturally this is all highly subjective.</p><h2 id="premium-build-too">Premium build, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8eL6o2trwA4CdbAWuBoEwK" name="Turtlebeach_StealthPro_II_013.JPG" alt="The Turtle Beach Stealth Pro II soft padded earcup pads" src="https://cdn.mos.cms.futurecdn.net/8eL6o2trwA4CdbAWuBoEwK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II looks and feels like a premium headset, too (as it should, given the price). The brushed metal earcup plates, gunmetal earcup carriers, matte black rubberized headband and mesh padding all scream quality. </p><p>Again, there’s very little between all the flagships here. You’ll find a touch of extra finesse and attention to detail on the Fractal Design Scape and BlackShark V3 Pro, but there’s seriously not much in it. The Maxwell 2 ANC feels as sturdy as they come, sure, but that’s mainly because it weighs a frankly preposterous 1.2lbs — it’s like comparing supercars to a dump truck. I’d say the Stealth Pro II feels a tad sturdier than the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/steelseries-arctis-nova-elite-review">Arctis Nova Elite</a> / Pro Omni, though. But did I mention there isn’t much in it? </p><h2 id="handy-base-station">Handy base station</h2><p>Man, I love me a base station. The Stealth Pro II’s is perhaps rather basic versus the Arctis Nova base station, which bundles a DAC for hi-res wireless, a multipoint USB-C hub, an EQ interface, a settings menu and a battery charging dock. </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:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ggKFe2mTDHGacW5py94Uic" name="Turtle Beach Stealth Pro II_test shots1" alt="The Turtle Beach Stealth Pro II base station with a charging battery and the status lights illuminated purple and green" src="https://cdn.mos.cms.futurecdn.net/ggKFe2mTDHGacW5py94Uic-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" 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 hey, the Stealth Pro II’s base station is still super handy, facilitating wireless hi-res audio and battery charging for no downtime (more on this later). I love the simplistic status lights, too. The outer light shows you the audio sample rate and/or mic mute status, while the inner light around the battery eject button displays the current charge of the docked power cell.</p><p>By contrast, the Audeze Maxwell 2 ANC and Razer BlackShark V3 Pro only come with a 2.4G dongle — and the Razer’s was particularly annoying in testing.</p><h2 id="39-zero-downtime-39-battery">'Zero-downtime' 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="SykCfeEQLDNVpgTarrTptK" name="Turtlebeach_StealthPro_II_008.JPG" alt="The Turtle Beach Stealth Pro II battery removed next to the open earcup" src="https://cdn.mos.cms.futurecdn.net/SykCfeEQLDNVpgTarrTptK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II is rated for up to 20 hours on a single charge by Turtle Beach, ANC off. This is ostensibly a little weak. The BlackShark V3 Pro is rated for up to 70 hours, while even the power-hungry Maxwell 2 ANC manages 40 hours without ANC. </p><p>However, from my testing, I think Turtle Beach is being a little conservative. After around 18 hours of use with ANC on, I still had around 65% left — partly, this is because the headset is really loud, meaning I only ran it at around 30% volume to hear everything and be comfortable. That saves juice.</p><p>Just like SteelSeries’ Arctis Nova headsets, though, the important factor is that the Stealth Pro II is a zero downtime headset. Its second battery charges in the base station as you play, meaning no meaningful downtime when gaming near the base station.</p><h2 id="competitively-priced">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aSSXJHNEycTaASgCARkhvK" name="Turtlebeach_StealthPro_II_010.JPG" alt="The Turtle Beach Stealth Pro II in hand" src="https://cdn.mos.cms.futurecdn.net/aSSXJHNEycTaASgCARkhvK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>It’d be remiss of me not to clarify here that the Stealth Pro II is objectively rather expensive at <a href="https://www.amazon.com/Turtle-Beach-Stealth-Premium-Wireless/dp/B0GN4NBD2V/" target="_blank" rel="nofollow">$349</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Stealth-Wireless-Multiplatform-Black/dp/B0GQCNJCPR" target="_blank" rel="nofollow">£299</a>. That said, it offers comparable performance and build quality to all its rivals, despite sitting in the middle of them all.</p><p>It’s cheaper than the $399 Arctis Nova Pro Omni and especially the $599 Elite, while offering comparatively strong sound quality. Its mic is just as good as the $449 Maxwell 2 ANC, and it’s a hell of a lot lighter and more breathable, even if it can’t match the sheer sound quality of Audeze’s planar drivers. Yet it’s cheaper than all the above.</p><p>The Stealth Pro II’s biggest foil is the $249 Razer BlackShark V3 Pro, which looks a little more premium and offers better ANC for around $100 less. But the Razer lacks the Stealth Pro II’s ‘zero downtime’ battery system and handy base station, and uses the Synapse companion software, which I like a whole lot less than Swarm II (to put it mildly).</p><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-the-downs"><span>Turtle Beach Stealth Pro II review: The downs</span></h2><p>It ain’t perfect, though. The Stealth Pro II’s biggest letdown for me was its ANC, which let through a lot of sound it shouldn’t. I also found the headset a little too clampy, although mileage may vary based on your head shape.</p><h2 id="a-little-too-clampy-for-me">A little too clampy for me</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YeKMHatYuYGg8XQZGNrEwK" name="Turtlebeach_StealthPro_II_009.JPG" alt="The Turtle Beach Stealth Pro II laying flat on a white surface with a blue background" src="https://cdn.mos.cms.futurecdn.net/YeKMHatYuYGg8XQZGNrEwK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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'll caveat this section by saying I found the Stealth Pro II mostly very comfortable. The earcups are super thick and padded, not to mention breathable. There's a layer of foam covering the drivers so even if your ears stick out, they won't rub against the plastic driver housing. And the mesh headband padding is a marvel — it’s supportive, light, breathable and effortlessly comfy. </p><p>Unfortunately, the headset is a little too clampy for me, though. After a few hours using it, I started to feel pressure on my temples and experience headaches. In fairness, this may reduce over time as the headset flexes, and mileage will vary based on the shape of your head. I’ve got a moderately large noggin, so ye of average headsize may be just fine.</p><h2 id="disappointing-anc">Disappointing ANC</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rsmr74xACd434fhrxJgMwK" name="Turtlebeach_StealthPro_II_006.JPG" alt="The Turtle Beach Stealth Pro II earcup pads" src="https://cdn.mos.cms.futurecdn.net/rsmr74xACd434fhrxJgMwK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>My biggest grip with the Stealth Pro II is its ANC. It has it, and it works to some extent, but it’s really rather poor versus rivals. The SteelSeries Arctis Nova Pro Omni and Elite, while their ANC isn’t perfect (and certainly doesn’t live up to the claims it can drown out a helicopter), performed much better than the Stealth Pro II in the same test. The Audeze Maxwell 2 ANC, meanwhile, is in a different league, as is the Razer BlackShark V3 Pro.</p><p>The Stealth Pro II, tested in my home office with traffic outside, let through substantial amounts of traffic noise. It also let through voices, keyboard sounds, mouse clicks, and chairs moving across the floor. It’s better than no ANC, but I wouldn’t recommend buying this headset if immersive and isolating ANC is your chief priority.</p><h2 id="what-s-that-smell">What’s that… smell?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="N6ujaJ4qU4bXnjgNscN7vK" name="Turtlebeach_StealthPro_II_003.JPG" alt="The Turtle Beach Stealth Pro II in its carry case" src="https://cdn.mos.cms.futurecdn.net/N6ujaJ4qU4bXnjgNscN7vK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Here’s a weird one: I’ve never had to say this about a headset before, but my Stealth Pro II smells bad. Like really bad. Of smoke or burning or something carcinogenic. It’s as though it was QC’d in a room where someone smokes 80-a-day… for a week.</p><p>I’m not sure whether this is to do with the materials used or the manufacturing process, or both. But it’s bad enough to be noticeable when holding the headset, and even when wearing it sometimes. At times it made me feel a little nauseous.</p><p>I’ve reached out to Turtle Beach’s representative to see why this may be. They’ve confirmed it was an issue with some pre-production models, but I’ve arranged a replacement full production unit to confirm it’s a one-off. I’ll update this review accordingly.</p><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-verdict"><span>Turtle Beach Stealth Pro II 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="3QuLvQvhuMrcGhzHBiY4vK" name="Turtlebeach_StealthPro_II_007.JPG" alt="The Turtle Beach Stealth Pro II against a blue background" src="https://cdn.mos.cms.futurecdn.net/3QuLvQvhuMrcGhzHBiY4vK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II is a really strong flagship offering from Turtle Beach. The sound quality is impressive, as is the mic, and I love the array of super useful features, from the handy companion app to the base station and ‘zero downtime’ battery. For Xbox and PC gamers, this is a highly commendable choice.</p><p>But it’s not perfect. The ANC system isn't quite up to scratch with the best in the biz, although that’s my only major issue. I found it a little clampy, but that’s really an anatomical roll of the dice, and my headset smelled rather weird, which is likely an isolated manufacturing issue. Given this headset comes in at a cool $50 cheaper than its chief rival, the Nova Pro Omni, they’re definitely things I can forgive.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/gaming/gaming-peripherals/turtle-beach-stealth-pro-ii-review</link>
                                                                            <description>
                            <![CDATA[ The Turtle Beach Stealth Pro II is a formidable flagship headset with sound quality to rival Razer and SteelSeries. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">gkTqoHMyLYWakTHQiDpMAU</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/mNY2NHgQPX2dFxhXExfcjK-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 04 Sep 2026 08:49:03 +0000</pubDate>                                                                                                                                <updated>Fri, 04 Sep 2026 08:49:07 +0000</updated>
                                                                                                                                            <category><![CDATA[Gaming Peripherals]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Gaming]]></category>
                                                    <category><![CDATA[Computing]]></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-320-70.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/mNY2NHgQPX2dFxhXExfcjK-1920-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The Turtle Beach Stealth Pro II next to its base station and 2.4G dongle]]></media:description>                                                            <media:text><![CDATA[The Turtle Beach Stealth Pro II next to its base station and 2.4G dongle]]></media:text>
                                <media:title type="plain"><![CDATA[The Turtle Beach Stealth Pro II next to its base station and 2.4G dongle]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/mNY2NHgQPX2dFxhXExfcjK-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Stealth Pro II is the flagship gaming headset from Turtle Beach, intended to compete with the heavy hitters from Razer, SteelSeries and Audeze. The <em>crème de la crème</em> are getting pricey now, with all the manufacturers above asking several hundred dollars for their very <a href="https://www.tomsguide.com/best-picks/best-gaming-headsets">best gaming headsets</a>. </p><p>The Stealth Pro II is no different, and hella pricey by objective standards, but actually comes in a little cheaper than some of its competition. It also offers excellent sound; an attractive, premium design; zero downtime battery; plus customization via a solid companion app. So could this be a fantastic balanced alternative to the main ultra-premium players?</p><p>As long as you don’t mind fairly weak ANC and a slightly tight clamp force! Find out more in my full Turtle Beach Stealth Pro II review.</p><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-cheat-sheet"><span>Turtle Beach Stealth Pro II review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A premium flagship gaming headset from Turtle Beach</li><li><strong>Who is it for? </strong>Competitive gamers and those who value sound quality</li><li><strong>What does it cost? </strong><a href="https://www.amazon.com/Turtle-Beach-Stealth-Premium-Wireless/dp/B0GN4NBD2V/" target="_blank" rel="nofollow">$349</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Stealth-Wireless-Multiplatform-Black/dp/B0GQCNJCPR" target="_blank" rel="nofollow">£299</a></li><li><strong>What do we like?</strong> The sound, mic, companion app, styling and build, base station and zero downtime battery system</li><li><strong>What don’t we like?</strong> The ANC is a little weak, and I found the headset a tad clampy</li></ul><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-specs"><span>Turtle Beach Stealth Pro II 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/Turtle-Beach-Stealth-Premium-Wireless/dp/B0GN4NBD2V/" target="_blank" rel="nofollow">$349</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Stealth-Wireless-Multiplatform-Black/dp/B0GQCNJCPR" target="_blank" rel="nofollow">£299</a></p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, white</p></td></tr><tr><td class="firstcol " ><p><strong>Platforms</strong></p></td><td  ><p>PC, Xbox, PlayStation, Bluetooth devices</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 3x 2.4Ghz, 1x Bluetooth</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>2x rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>40 hours combined</p></td></tr><tr><td class="firstcol " ><p><strong>Frequency response</strong></p></td><td  ><p>10-40,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Drivers</strong></p></td><td  ><p>60mm coaxial</p></td></tr><tr><td class="firstcol " ><p><strong>Multipoint connectivity</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>4 (3x 2.4G, 1x BT)</p></td></tr><tr><td class="firstcol " ><p><strong>Microphone</strong></p></td><td  ><p>Unidirectional floating boom mic</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>13.8 ounces / 393 grams</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-the-ups"><span>Turtle Beach Stealth Pro II review: The ups</span></h2><p>There’s a lot to love about the Stealth Pro II. Sound quality is excellent, there’s a useful companion app, a super clear mic, and a handy base station for ‘zero downtime’ battery life. It’s also moderately priced versus competition.</p><h2 id="excellent-sound-quality">Excellent 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:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HFWdi2AigrzMKFpu3MX5bc" name="Turtle Beach Stealth Pro II_test shots2" alt="The Turtle Beach Stealth Pro II with an earcup removed showing the coaxial driver" src="https://cdn.mos.cms.futurecdn.net/HFWdi2AigrzMKFpu3MX5bc-1920-80.jpg" mos="" align="middle" fullscreen="1" width="2560" height="1440" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/HFWdi2AigrzMKFpu3MX5bc-1920-80.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">Coaxial drivers feature a tweeter that sits in the middle/on top of a main woofer. The tweeter sits under that central circular housing, while the main woofer is the larger, fluted circle. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The Turtle Beach Stealth Pro II’s biggest strength is its sound quality. It uses a coaxial driver setup, with a 60mm woofer and separate tweeter in each earcup. This, in theory, gives better clarity and more separation between higher and lower frequencies, as each driver can focus on its own job. Connectivity-wise, there’s 24-bit/96kHz high resolution data/sampling rates over 2.4G, so this is a true hi-res headset, akin to the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/steelseries-arctis-nova-pro-omni-review">SteelSeries Arctis Nova Pro Omni</a> / Elite and <a href="https://www.tomsguide.com/gaming/gaming-peripherals/audeze-maxwell-2-anc-review">Audeze Maxwell 2 ANC</a>.</p><p>The sound is super spacious with exceptional clarity up top and very strong imaging (this lets you pinpoint noises to a specific point). This naturally benefits competitive gameplay, letting me hear the location of enemy cues like footsteps or reloads in my test FPS games: Counter Strike 2 and Isonzo. It was also handy for stealthy games like Sniper Elite: Resistance, where I could again hear and locate even the quietest tapping of enemy footsteps. And it was ideal for immersion, giving three-dimensionality to sounds and effects in the campaign and battles of Total War: Rome II.</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="mXPzdXE3NLgxLtw9ZvfbHc" name="Turtle Beach Stealth Pro II_screenshots3" alt="A screenshot of the Turtle Beach Swarm II app being used to customize the Stealth Pro II" src="https://cdn.mos.cms.futurecdn.net/mXPzdXE3NLgxLtw9ZvfbHc-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/mXPzdXE3NLgxLtw9ZvfbHc-1920-80.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">Via the Swarm II app, you have access to a preset Footsteps EQ for a competitive edge. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Turtle Beach / Future)</span></figcaption></figure><p>There’s also Dolby Atmos (DA) support, so you can benefit from that codec in games that use it for surround sound. But even without DA, the sound is impressively spatial.</p><p>Tonal separation and mid-range clarity are really excellent, too. Each of the instruments and layers of the beautiful Rome II soundtrack sounded isolated yet cohesive — the panpipes, the atmospheric flutes, the drums, the horns, all occupied their own distinct space.</p><p>And thanks to that hefty 60mm main driver, there’s plenty of bass performance, giving strong, impactful artillery explosions in Isonzo. Bass response is not quite as nicely controlled as on the Arctis Nova Pro Omni or Elite, and you don’t get anywhere near the tightness of the Maxwell 2’s low-end. I noticed a little bit of booming and muddying at times, too, but it was nothing major, and generally, I loved listening to games and music on the Stealth Pro II.</p><p><em>Listen along to my audio testing playlist on </em><a href="https://www.tomsguide.com/audio/qobuz-review"><em>Qobuz</em></a><em>, our favorite </em><a href="https://www.tomsguide.com/buying-guide/best-music-streaming-services"><em>music streaming service</em></a><em> for audiophiles.</em></p><iframe allow="" height="390" width="400" id="" style="" class="position-center" data-lazy-priority="high" data-lazy-src="https://widget.qobuz.com/playlist/59666746?zone=GB-en"></iframe><h2 id="useful-companion-app">Useful companion app</h2><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="WKivUJJSayR32JEJFmsJPc" name="Turtle Beach Stealth Pro II_screenshots4" alt="A screenshot of the Turtle Beach Swarm II app being used to customize the Stealth Pro II EQ" src="https://cdn.mos.cms.futurecdn.net/WKivUJJSayR32JEJFmsJPc-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/WKivUJJSayR32JEJFmsJPc-1920-80.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: Turtle Beach / Future)</span></figcaption></figure><p>Complementing the Stealth Pro II’s sound is an equally strong companion app, which gives you access to a full custom EQ, as well as EQ presets and a (<em>cringe</em>) “Superhuman hearing” mode — essentially some EQ presets to enhance things like footsteps.</p><p>There’s not the same enormous range of presets as on the SteelSeries GG app, with its hundreds-strong library of per-game EQs. But there’s still more than enough control in Swarm II to get the most out of your game audio.</p><p>In the app, you can also control the microphone EQ, update the headset and (my favorite aspect) change the control assignments to customize how the headset operates. So useful!</p><h2 id="multipoint-connectivity">Multipoint 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:1181px;"><p class="vanilla-image-block" style="padding-top:56.31%;"><img id="phhk8xiqF6fCKSfmxYZ5vK" name="Turtlebeach_StealthPro_II_012.JPG" alt="The Turtle Beach Stealth Pro II earcup and controls" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:380,l:413,cw:1181,ch:665,q:80/phhk8xiqF6fCKSfmxYZ5vK.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II offers multipoint connectivity. You can hook up two devices over 2.4G out of the box, using the base station and included USB dongle. You can also buy more dongles (the carry case even comes with a spare dongle slot) to connect to even more, up to four devices. </p><p>If that weren’t enough, you can also connect over Bluetooth simultaneously. I found this a little annoying, though, as the headset doesn’t mute game sounds to prioritize Bluetooth if, for example, you get a call or need to take a work meeting while gaming (which, as you can imagine, happens a lot in my job). You just have both going on at once, and the sidetone dial didn't work to fix this. It’s not a huge issue, but I like that the Nova Pro Omni and Maxwell 2 prioritize Bluetooth in those scenarios.</p><h2 id="super-clear-mic">Super clear mic</h2><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="3EgUuzZLcuowP7EX98QduK" name="Turtlebeach_StealthPro_II_014.JPG" alt="The Turtle Beach Stealth Pro II next to its base station and 2.4G dongle" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:540,l:476,cw:960,ch:540,q:80/3EgUuzZLcuowP7EX98QduK.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II uses a unidirectional mic that’s “floating,” meaning it’s suspended on rubber spokes to eliminate vibration. Turtle Beach says this gives exceptional clarity, although in fairness it’s just a great mic anyway, floating or otherwise.</p><p>Headset mics, even flagship ones, usually disappoint, so I was very surprised at the cleanness and airiness of voice recordings. There was arguably a little too much clarity, as I started to pick up on some of my inherent lispy and sibilant qualities — but there’s no denying how clean and noise-free the recordings were, especially using the ‘clarity’ mic EQ. </p><p>There’s environmental noise reduction, but being a unidirectional mic, that isn’t really needed — the mic only picks up sound from your mouth anyway. I recorded test clips with my window open to a very busy and loud road. Even with noise reduction off, I couldn’t hear the road in any recordings.</p><h2 id="stealthy-grown-up-design">Stealthy, grown-up 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="9im4tknZx6qrX7paMavoqK" name="Turtlebeach_StealthPro_II_011.JPG" alt="The Turtle Beach Stealth Pro II earcup showing the controls" src="https://cdn.mos.cms.futurecdn.net/9im4tknZx6qrX7paMavoqK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Turtle Beach gear is very often styled a little ‘gamer-y’ for my tastes. Even the pricey stuff is typically a bit garish for me, like the $200 <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb7-tkl-review">Turtle Beach Command KB7</a> we tested before this headset. So I was pleasantly surprised by how grown up the Stealth Pro II feels. The eponymous, stealthy, blacked-out look means it won’t seem out of place in any setup — whether you’re an RGB-clad unicorn puke kinda person, or an anti-RGBer like me.</p><p>I definitely prefer the look of the Stealth Pro II to the chunky Maxwell 2 ANC, and I’d say it edges the Arctis Nova Pro Omni and Elite (although there’s not much in it). For sheer prettiness, the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/razer-blackshark-v3-pro-review">Razer BlackShark V3 Pro</a> and <a href="https://www.tomsguide.com/gaming/gaming-peripherals/fractal-design-scape-review">Fractal Design Scape</a> still win out for me, but naturally this is all highly subjective.</p><h2 id="premium-build-too">Premium build, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8eL6o2trwA4CdbAWuBoEwK" name="Turtlebeach_StealthPro_II_013.JPG" alt="The Turtle Beach Stealth Pro II soft padded earcup pads" src="https://cdn.mos.cms.futurecdn.net/8eL6o2trwA4CdbAWuBoEwK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II looks and feels like a premium headset, too (as it should, given the price). The brushed metal earcup plates, gunmetal earcup carriers, matte black rubberized headband and mesh padding all scream quality. </p><p>Again, there’s very little between all the flagships here. You’ll find a touch of extra finesse and attention to detail on the Fractal Design Scape and BlackShark V3 Pro, but there’s seriously not much in it. The Maxwell 2 ANC feels as sturdy as they come, sure, but that’s mainly because it weighs a frankly preposterous 1.2lbs — it’s like comparing supercars to a dump truck. I’d say the Stealth Pro II feels a tad sturdier than the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/steelseries-arctis-nova-elite-review">Arctis Nova Elite</a> / Pro Omni, though. But did I mention there isn’t much in it? </p><h2 id="handy-base-station">Handy base station</h2><p>Man, I love me a base station. The Stealth Pro II’s is perhaps rather basic versus the Arctis Nova base station, which bundles a DAC for hi-res wireless, a multipoint USB-C hub, an EQ interface, a settings menu and a battery charging dock. </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:2560px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ggKFe2mTDHGacW5py94Uic" name="Turtle Beach Stealth Pro II_test shots1" alt="The Turtle Beach Stealth Pro II base station with a charging battery and the status lights illuminated purple and green" src="https://cdn.mos.cms.futurecdn.net/ggKFe2mTDHGacW5py94Uic-1920-80.jpg" mos="" align="middle" fullscreen="" width="2560" height="1440" 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 hey, the Stealth Pro II’s base station is still super handy, facilitating wireless hi-res audio and battery charging for no downtime (more on this later). I love the simplistic status lights, too. The outer light shows you the audio sample rate and/or mic mute status, while the inner light around the battery eject button displays the current charge of the docked power cell.</p><p>By contrast, the Audeze Maxwell 2 ANC and Razer BlackShark V3 Pro only come with a 2.4G dongle — and the Razer’s was particularly annoying in testing.</p><h2 id="39-zero-downtime-39-battery">'Zero-downtime' 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="SykCfeEQLDNVpgTarrTptK" name="Turtlebeach_StealthPro_II_008.JPG" alt="The Turtle Beach Stealth Pro II battery removed next to the open earcup" src="https://cdn.mos.cms.futurecdn.net/SykCfeEQLDNVpgTarrTptK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II is rated for up to 20 hours on a single charge by Turtle Beach, ANC off. This is ostensibly a little weak. The BlackShark V3 Pro is rated for up to 70 hours, while even the power-hungry Maxwell 2 ANC manages 40 hours without ANC. </p><p>However, from my testing, I think Turtle Beach is being a little conservative. After around 18 hours of use with ANC on, I still had around 65% left — partly, this is because the headset is really loud, meaning I only ran it at around 30% volume to hear everything and be comfortable. That saves juice.</p><p>Just like SteelSeries’ Arctis Nova headsets, though, the important factor is that the Stealth Pro II is a zero downtime headset. Its second battery charges in the base station as you play, meaning no meaningful downtime when gaming near the base station.</p><h2 id="competitively-priced">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="aSSXJHNEycTaASgCARkhvK" name="Turtlebeach_StealthPro_II_010.JPG" alt="The Turtle Beach Stealth Pro II in hand" src="https://cdn.mos.cms.futurecdn.net/aSSXJHNEycTaASgCARkhvK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>It’d be remiss of me not to clarify here that the Stealth Pro II is objectively rather expensive at <a href="https://www.amazon.com/Turtle-Beach-Stealth-Premium-Wireless/dp/B0GN4NBD2V/" target="_blank" rel="nofollow">$349</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Stealth-Wireless-Multiplatform-Black/dp/B0GQCNJCPR" target="_blank" rel="nofollow">£299</a>. That said, it offers comparable performance and build quality to all its rivals, despite sitting in the middle of them all.</p><p>It’s cheaper than the $399 Arctis Nova Pro Omni and especially the $599 Elite, while offering comparatively strong sound quality. Its mic is just as good as the $449 Maxwell 2 ANC, and it’s a hell of a lot lighter and more breathable, even if it can’t match the sheer sound quality of Audeze’s planar drivers. Yet it’s cheaper than all the above.</p><p>The Stealth Pro II’s biggest foil is the $249 Razer BlackShark V3 Pro, which looks a little more premium and offers better ANC for around $100 less. But the Razer lacks the Stealth Pro II’s ‘zero downtime’ battery system and handy base station, and uses the Synapse companion software, which I like a whole lot less than Swarm II (to put it mildly).</p><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-the-downs"><span>Turtle Beach Stealth Pro II review: The downs</span></h2><p>It ain’t perfect, though. The Stealth Pro II’s biggest letdown for me was its ANC, which let through a lot of sound it shouldn’t. I also found the headset a little too clampy, although mileage may vary based on your head shape.</p><h2 id="a-little-too-clampy-for-me">A little too clampy for me</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YeKMHatYuYGg8XQZGNrEwK" name="Turtlebeach_StealthPro_II_009.JPG" alt="The Turtle Beach Stealth Pro II laying flat on a white surface with a blue background" src="https://cdn.mos.cms.futurecdn.net/YeKMHatYuYGg8XQZGNrEwK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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'll caveat this section by saying I found the Stealth Pro II mostly very comfortable. The earcups are super thick and padded, not to mention breathable. There's a layer of foam covering the drivers so even if your ears stick out, they won't rub against the plastic driver housing. And the mesh headband padding is a marvel — it’s supportive, light, breathable and effortlessly comfy. </p><p>Unfortunately, the headset is a little too clampy for me, though. After a few hours using it, I started to feel pressure on my temples and experience headaches. In fairness, this may reduce over time as the headset flexes, and mileage will vary based on the shape of your head. I’ve got a moderately large noggin, so ye of average headsize may be just fine.</p><h2 id="disappointing-anc">Disappointing ANC</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rsmr74xACd434fhrxJgMwK" name="Turtlebeach_StealthPro_II_006.JPG" alt="The Turtle Beach Stealth Pro II earcup pads" src="https://cdn.mos.cms.futurecdn.net/rsmr74xACd434fhrxJgMwK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>My biggest grip with the Stealth Pro II is its ANC. It has it, and it works to some extent, but it’s really rather poor versus rivals. The SteelSeries Arctis Nova Pro Omni and Elite, while their ANC isn’t perfect (and certainly doesn’t live up to the claims it can drown out a helicopter), performed much better than the Stealth Pro II in the same test. The Audeze Maxwell 2 ANC, meanwhile, is in a different league, as is the Razer BlackShark V3 Pro.</p><p>The Stealth Pro II, tested in my home office with traffic outside, let through substantial amounts of traffic noise. It also let through voices, keyboard sounds, mouse clicks, and chairs moving across the floor. It’s better than no ANC, but I wouldn’t recommend buying this headset if immersive and isolating ANC is your chief priority.</p><h2 id="what-s-that-smell">What’s that… smell?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="N6ujaJ4qU4bXnjgNscN7vK" name="Turtlebeach_StealthPro_II_003.JPG" alt="The Turtle Beach Stealth Pro II in its carry case" src="https://cdn.mos.cms.futurecdn.net/N6ujaJ4qU4bXnjgNscN7vK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Here’s a weird one: I’ve never had to say this about a headset before, but my Stealth Pro II smells bad. Like really bad. Of smoke or burning or something carcinogenic. It’s as though it was QC’d in a room where someone smokes 80-a-day… for a week.</p><p>I’m not sure whether this is to do with the materials used or the manufacturing process, or both. But it’s bad enough to be noticeable when holding the headset, and even when wearing it sometimes. At times it made me feel a little nauseous.</p><p>I’ve reached out to Turtle Beach’s representative to see why this may be. They’ve confirmed it was an issue with some pre-production models, but I’ve arranged a replacement full production unit to confirm it’s a one-off. I’ll update this review accordingly.</p><h2 class="article-body__section" id="section-turtle-beach-stealth-pro-ii-review-verdict"><span>Turtle Beach Stealth Pro II 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="3QuLvQvhuMrcGhzHBiY4vK" name="Turtlebeach_StealthPro_II_007.JPG" alt="The Turtle Beach Stealth Pro II against a blue background" src="https://cdn.mos.cms.futurecdn.net/3QuLvQvhuMrcGhzHBiY4vK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Stealth Pro II is a really strong flagship offering from Turtle Beach. The sound quality is impressive, as is the mic, and I love the array of super useful features, from the handy companion app to the base station and ‘zero downtime’ battery. For Xbox and PC gamers, this is a highly commendable choice.</p><p>But it’s not perfect. The ANC system isn't quite up to scratch with the best in the biz, although that’s my only major issue. I found it a little clampy, but that’s really an anatomical roll of the dice, and my headset smelled rather weird, which is likely an isolated manufacturing issue. Given this headset comes in at a cool $50 cheaper than its chief rival, the Nova Pro Omni, they’re definitely things I can forgive.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Yes, this 8-SSD storage unit costs more than a car but packs up to 256TB — three things I like about the Seagate LaCie behemoth and two I don't ]]></title>
                                                                                                <dc:content><![CDATA[ <p>As someone who reviews the <a href="https://www.tomsguide.com/us/best-cameras,review-2196.html">best cameras</a> and the <a href="https://www.tomsguide.com/us/best-drones,review-2412.html">best drones</a> on the regular, I'm no stranger to massive files that eat into my laptop's small storage. Whenever I review a camera or drone that records 6K or 8K video, my laptop shakes in its boots. Testing a 100MP medium-format camera that captures RAW files over 200MB each, like the <a href="https://www.tomsguide.com/cameras-photography/mirrorless-cameras/fujifilm-gfx100rf-review">Fujifilm GFX100RF</a>? A tear falls down my laptop's lid.</p><p>I know what you're thinking. "Just get an external SSD!" And you'd be right in suggesting that. Thankfully, as part of my job, I get to test all kinds of cool gadgets — and I think I've just tested one of the coolest digital storage systems, or Direct-Attached Storage more commonly known as DAS, (a lot of) money can buy right now. The <a href="https://www.seagate.com/gb/en/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">Seagate LaCie 8big Pro5</a> is certainly big and definitely designed for pros —  and it's got a hefty price to match.</p><div class="product"><a data-dimension112="08441344-a22b-11f1-a03d-fdf254c0ae2e" data-action="Deal Block" data-label="LaCie 8big Pro5" data-dimension48="LaCie 8big Pro5" data-dimension25="$6812" href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:921px;"><p class="vanilla-image-block" style="padding-top:77.96%;"><img id="M5BH5JAhQEdP4tR5X3FLaa" name="Seagate LaCie 8big Pro5 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/M5BH5JAhQEdP4tR5X3FLaa-1920-80.jpg" mos="" align="middle" fullscreen="" width="921" height="718" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The cheapest Seagate LaCie 8big Pro5 model costs over $6,000. Regardless of the capacity you get (32TB to 256TB), you get a rugged design, eight hot-swappable bays, read and write speeds up to 2,800MBps, Thunderbolt 5 compatibility, and lots more.<a class="view-deal button" href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow" data-dimension112="08441344-a22b-11f1-a03d-fdf254c0ae2e" data-action="Deal Block" data-label="LaCie 8big Pro5" data-dimension48="LaCie 8big Pro5" data-dimension25="$6812">View Deal</a></p></div><p>So, what exactly is the LaCie 8big Pro5? Well, it's a high-capacity DAS system featuring eight hot-swappable bays that house IronWolf Pro hard drives of varying capacities depending on the model you buy. The cheapest you can get is the <a href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow">32TB model ($6,812)</a>, while the most expensive is the <a href="https://www.seagate.com/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">256TB model, which costs a whopping $14,259</a>. The LaCie 8big Pro5 is designed for professional creative workflows, video production, and broadcast environments requiring real-time editing of 4K, 6K or 8K footage.</p><p>Or for anyone who really, <em>really </em>needs the storage and doesn't mind splurging on it. I've spent a couple of weeks testing and using the LaCie 8big Pro5 and I'm impressed. It makes me feel like I'll never need storage again for generations. Here are three things I like about it and two I don't.</p><h2 class="article-body__section" id="section-what-i-like"><span>What I like</span></h2><h2 id="tank-like-and-hot-swappable-design">Tank-like and hot-swappable 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:6632px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="X3vs2aWDeeF2Xy7haqWW4H" name="Seagate_Lacie-oped-5" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/X3vs2aWDeeF2Xy7haqWW4H-1920-80.jpg" mos="" align="middle" fullscreen="" width="6632" height="3730" 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>When the Seagate LaCie 8big Pro5 arrived at my office and I tried to lift up the cardboard box, I was afraid I was going to pull my back out. That's because the LaCie 8big Pro is built like a tank. This thing is a beast, made of sturdy, solid, single-piece aluminum with a metallic gray finish. I'm certain it could stand the test of time and you'd find it, intact, under the rubble of a demolished building in the post-apocalyptic world.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/QEB3xsnmoJK3qmzJt5JGzG-1920-80.jpg" alt="A Seagate LaCie 8big Pro5 DAS" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/KDNGQ65iPJADLgPRYspdoG-1920-80.jpg" alt="A Seagate LaCie 8big Pro5 DAS" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure></figure><p>The LaCie 8big Pro5 is a chunky DAS, measuring 11.69 x 9.13 x 8.46 inches and weighing a whopping 31.59lbs. To make moving it around easier, it features an integrated carrying handle on the top. I highly doubt you'd be doing much traveling with it, though. But hey, if you lifted this thing two or three times, you could probably skip arm day at the gym.</p><p>This DAS is hot-swappable, too. Now, as someone who tests the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> day in and out, I could kind of gauge what 'hot-swappable' meant in this context. On a keyboard, it means that you can change the switches by simply popping them out and not having to de- and re-solder them to the PCB. It's a similar concept on the LaCie 8big Pro5. You can essentially remove and replace the eight individual hard drives while the storage system remains powered on and running, and your projects and data transfers keep running on the remaining active drives.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/88HLKKCyZx8L8tQTp22A5Q-1920-80.jpg" alt="Seagate LaCie 8big Pro5 DAS screenshot" /><figcaption><small role="credit">Seagate / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/k4qdFLWv77wcEtMxwQv68Q-1920-80.jpg" alt="Seagate LaCie 8big Pro5 DAS screenshot" /><figcaption><small role="credit">Seagate / Tom's Guide</small></figcaption></figure></figure><p>I also love the LaCie 8big Pro5's Redundancy Protection. When using a fault-tolerant RAID mode, like RAID 5 or RAID 6, the system uses parity data to keep your files safe while a broken drive is missing.</p><p>You needn't worry about manually getting a replacement drive up and running either, as <a href="https://www.seagate.com/support/software/raid-manager/" target="_blank" rel="nofollow">LaCie RAID Manager</a> automatically rebuilds the data array for you. As someone who's a newbie to this stuff, I appreciate that I don't have to wrap my head around a plethora of technical terms — the software does it for me.</p><h2 id="ridiculous-read-and-write-speeds">Ridiculous read and write speeds</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7279px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="sDx59vzk2KZWN9tLQ3hE4H" name="Seagate_Lacie-oped-4" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/sDx59vzk2KZWN9tLQ3hE4H-1920-80.jpg" mos="" align="middle" fullscreen="" width="7279" height="4094" 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>A storage system that's designed to aid creative workflows wouldn't mean much if it didn't boast fast read and write speeds — and thankfully, the Seagate LaCie 8big Pro5 does just that. I ran a disk speed test using CrystalDiskMark and the numbers speak for themselves. Read and write speeds are more dependant on the hard drives themselves, rather than the enclosure, and I tested the LaCie 8big Pro5 with Seagate IronWolf Pro 24TB Enterprise NAS HDDs (<a href="https://www.amazon.com/Seagate-IronWolf-Enterprise-Internal-Drive/dp/B0CSPCFKR9/" target="_blank" rel="nofollow">$859 each</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:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="LxyQHLu6HdA6RbjbjffcAQ" name="Seagate_Lacie_oped-10" alt="CrystalDiskMark screenshot" src="https://cdn.mos.cms.futurecdn.net/LxyQHLu6HdA6RbjbjffcAQ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/LxyQHLu6HdA6RbjbjffcAQ-1920-80.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: CrystalDiskMark / Tom's Guide)</span></figcaption></figure><p>As you can see from the screenshot above, the LaCie 8big Pro5 boasts <strong>2,928MBps read and 1,687MBps write speeds</strong> (Q8T1). These are great for copying movies, video files, or gargantuan RAW images. The <strong>2,387MBps read and 1,572MBps write speeds</strong> in Q1T1 represent standard real-world transfer speeds when copying single large files one at a time.</p><p>These numbers basically tell you that the LaCie 8big Pro5 is healthy and operating at high speeds, and the DAS delivers fast boot times, rapid game load screens, and smooth handling of heavy file transfers. Overall, the LaCie 8big Pro5 performs formidably and proves its worth in complex workflows.</p><h2 id="doubles-as-a-charger">Doubles as a charger!</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4146px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AsDKo6kUR8PPcEqhL5RgLG" name="Seagate_Lacie-oped-2" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/AsDKo6kUR8PPcEqhL5RgLG-1920-80.jpg" mos="" align="middle" fullscreen="" width="4146" height="2332" 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 don't know about you but I really don't like multiple cables cluttering my desk and making my setup look messy. A good cable management solution is a dock, and the Seagate LaCie 8big Pro5's got one built it in — it's a DAS that essentially doubles as a charger.</p><p>Located on the back are four USB-C ports. One of these is a Thunderbolt 5 port with 140W of power delivery, so you can power your Windows laptop or MacBook without an dedicated charger. There are also two downstream ports for connecting external displays (up to two 8K monitors) or daisy-chaining additional Thunderbolt devices with up to 30W PD per port.</p><p>The last is a downstream USB 3.2 Gen 2 port with speeds up to 20Gbps and 15W PD, and it's meant for your peripherals, like mice and keyboards. The 20Gbps speed is excellent as it made quick work of transferring big 4K video files from the <a href="https://www.tomsguide.com/cameras-photography/sony-rx10-v-review">Sony RX10 V</a>. The LaCie 8big Pro5 basically packs everything you could ask for to maintain a neat and tidy setup. </p><h2 class="article-body__section" id="section-what-i-don-t-like"><span>What I don't like</span></h2><h2 id="the-eye-watering-price-tag">The eye-watering price tag</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6870px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Lh3Ku7nHPB25vUym4rAkBH" name="Seagate_Lacie-oped-9" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/Lh3Ku7nHPB25vUym4rAkBH-1920-80.jpg" mos="" align="middle" fullscreen="" width="6870" height="3864" 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>Direct-Attached Storage systems don't come cheap, and the same goes for the Seagate LaCie 8big Pro5. Given that this DAS is designed for creative professionals, film studios, and perhaps game developers, it's no surprise that it costs an arm and a leg. Given that, it would be unfair to mark it down for the premium price tag, but it's worth pointing out anyway.</p><p>The LaCie 8big Pro5 starts at <a href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow">$6,812</a> for the 32TB model and goes up to <a href="https://www.seagate.com/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">$14,259</a> for the 256TB model. I'll give you a moment to take it in. If you want a breakdown of all the models, I've done the homework for you:</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Model</strong></p></th><th  ><p>Price in USD</p></th><th  ><p>Price in GBP</p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>32TB</strong></p></td><td  ><p><a href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow">$6,812</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-32tb-3303665/" target="_blank" rel="nofollow">£6,169</a></p></td></tr><tr><td class="firstcol " ><p><strong>64TB</strong></p></td><td  ><p><a href="https://www.antonline.com/Seagate/Miscellaneous/1562938" target="_blank" rel="nofollow">$8,005</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-64tb-3303666/" target="_blank" rel="nofollow">£7,469</a></p></td></tr><tr><td class="firstcol " ><p><strong>128TB</strong></p></td><td  ><p><a href="https://www.antonline.com/Seagate/Miscellaneous/1562936" target="_blank" rel="nofollow">$9,867</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-128tb-3303667/" target="_blank" rel="nofollow">£7,999</a></p></td></tr><tr><td class="firstcol " ><p><strong>192TB</strong></p></td><td  ><p><a href="https://www.antonline.com/Seagate/Miscellaneous/1562937" target="_blank" rel="nofollow">$12,394</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-192tb-3303668/" target="_blank" rel="nofollow">£11,589</a></p></td></tr><tr><td class="firstcol " ><p><strong>256TB</strong></p></td><td  ><p><a href="https://www.seagate.com/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">$14,259</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-256tb-3303669/" target="_blank" rel="nofollow">£14,709</a></p></td></tr></tbody></table></div><p>Yep, it's a lot. When I showed this table to my coworker and fellow <a href="https://www.tomsguide.com/uk/author/erin-bashford">senior reviews writer, Erin</a>, she exclaimed over Slack, "You could buy ALMOST FIVE of my cars for that. That's 4.6 2012 Volkswagen Polos for £14,000. And my old car? You could get FORTY SIX 2007 Fiat Pandas for £14K."</p><p>Thankfully, though, should something go wrong with your LaCie 8big Pro5, Seagate happily offers a 5-year warranty and Rescue Data Recovery services (but it isn't available in all countries). </p><h2 id="e-waste-anyone">E-waste, anyone?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6959px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Z3oVT7V3ezsWhg8Wm9iH6H" name="Seagate_Lacie-oped" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/Z3oVT7V3ezsWhg8Wm9iH6H-1920-80.jpg" mos="" align="middle" fullscreen="" width="6959" height="3914" 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 might be a minor complaint and it might not matter to many people — but it does to me. The Seagate LaCie 8big Pro5 comes with not one, not two, not three, but <em>four </em>power cords for various international standards. I can kind of understand why that is. If you work for a film studio and are constantly lugging hard drives and storage units on planes or other modes of transport, perhaps you'd need — not one, not two, not three — four international power cables.</p><p>I, personally, find it a little frustrating, as most people will end up using just one power cord — as I did the U.K. one in my testing — and will probably get rid of the other three. These will then end up as e-waste in a landfill somewhere. As an environmentally-conscious person who tries to do her part to save the planet, it's annoying. I wish Seagate gave you the option to pick the power cords you needed most at checkout.</p><h2 class="article-body__section" id="section-looking-ahead"><span>Looking ahead...</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:7425px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="zdWBMfSRjGxzuB2JXDWMCH" name="Seagate_Lacie-oped-6" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/zdWBMfSRjGxzuB2JXDWMCH-1920-80.jpg" mos="" align="middle" fullscreen="" width="7425" height="4177" 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 you're someone who deals with massive video files and RAW images, and you need massive storage capacity and ultra-fast data speeds, the Seagate LaCie 8big Pro5 is the DAS for you. I've thoroughly enjoyed my time with it. It's powerful and it's built like a tank, and it's hot-swappable design means you can remove the individual SSDs without worrying about data corruption.</p><p>I also love that the LaCie 8big Pro5 essentially doubles as a dock, or a charger, for all your peripherals and external displays, which results in effective cable management. If you've got $7,000 saved up and need a competent DAS, this is the one to get.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xk8gMe"></div>                            </div>                            <script src="https://kwizly.com/embed/Xk8gMe.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/best-picks/best-external-hard-drives">These are the 5 best external SSDs we've tested to free up space on your laptop and phone</a></li><li><a href="https://www.tomsguide.com/computing/the-coolest-piece-of-tech-i-bought-all-year-was-the-dxp4800-plus-heres-why-its-a-game-changer">The coolest piece of tech I bought all year was the DXP4800 Plus — here’s why it’s a game changer</a></li><li><a href="https://www.tomsguide.com/cameras-photography/sony-rx10-v-review">I’ve taken over 3,000 photos on the Sony RX10 V — its 25x zoom lens and 30fps blackout-free burst make it the king of bridge cameras</a></li></ul><div data-widget-type="review" data-model-name="LaCie 8big Pro5"></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/seagate-lacie-8big-pro5-hands-on</link>
                                                                            <description>
                            <![CDATA[ I've spent over two weeks testing the Seagate LaCie 8big Pro5 with its gargantuan 256TB of storage, and it's an excellent storage system... for rich people. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">KbUrob6i4pyceiby8fMEic</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/kojCdGUQamS7BikrA5PyCH-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 29 Aug 2026 11:30:00 +0000</pubDate>                                                                                                                                <updated>Tue, 01 Sep 2026 10:17:20 +0000</updated>
                                                                                                                                            <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/kojCdGUQamS7BikrA5PyCH-1920-80.jpg">
                                                            <media:credit><![CDATA[Nikita Achanta / Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Seagate LaCie 8big Pro5 DAS]]></media:description>                                                            <media:text><![CDATA[A Seagate LaCie 8big Pro5 DAS]]></media:text>
                                <media:title type="plain"><![CDATA[A Seagate LaCie 8big Pro5 DAS]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/kojCdGUQamS7BikrA5PyCH-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>As someone who reviews the <a href="https://www.tomsguide.com/us/best-cameras,review-2196.html">best cameras</a> and the <a href="https://www.tomsguide.com/us/best-drones,review-2412.html">best drones</a> on the regular, I'm no stranger to massive files that eat into my laptop's small storage. Whenever I review a camera or drone that records 6K or 8K video, my laptop shakes in its boots. Testing a 100MP medium-format camera that captures RAW files over 200MB each, like the <a href="https://www.tomsguide.com/cameras-photography/mirrorless-cameras/fujifilm-gfx100rf-review">Fujifilm GFX100RF</a>? A tear falls down my laptop's lid.</p><p>I know what you're thinking. "Just get an external SSD!" And you'd be right in suggesting that. Thankfully, as part of my job, I get to test all kinds of cool gadgets — and I think I've just tested one of the coolest digital storage systems, or Direct-Attached Storage more commonly known as DAS, (a lot of) money can buy right now. The <a href="https://www.seagate.com/gb/en/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">Seagate LaCie 8big Pro5</a> is certainly big and definitely designed for pros —  and it's got a hefty price to match.</p><div class="product"><a data-dimension112="08441344-a22b-11f1-a03d-fdf254c0ae2e" data-action="Deal Block" data-label="LaCie 8big Pro5" data-dimension48="LaCie 8big Pro5" data-dimension25="$6812" href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:921px;"><p class="vanilla-image-block" style="padding-top:77.96%;"><img id="M5BH5JAhQEdP4tR5X3FLaa" name="Seagate LaCie 8big Pro5 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/M5BH5JAhQEdP4tR5X3FLaa-1920-80.jpg" mos="" align="middle" fullscreen="" width="921" height="718" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The cheapest Seagate LaCie 8big Pro5 model costs over $6,000. Regardless of the capacity you get (32TB to 256TB), you get a rugged design, eight hot-swappable bays, read and write speeds up to 2,800MBps, Thunderbolt 5 compatibility, and lots more.<a class="view-deal button" href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow" data-dimension112="08441344-a22b-11f1-a03d-fdf254c0ae2e" data-action="Deal Block" data-label="LaCie 8big Pro5" data-dimension48="LaCie 8big Pro5" data-dimension25="$6812">View Deal</a></p></div><p>So, what exactly is the LaCie 8big Pro5? Well, it's a high-capacity DAS system featuring eight hot-swappable bays that house IronWolf Pro hard drives of varying capacities depending on the model you buy. The cheapest you can get is the <a href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow">32TB model ($6,812)</a>, while the most expensive is the <a href="https://www.seagate.com/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">256TB model, which costs a whopping $14,259</a>. The LaCie 8big Pro5 is designed for professional creative workflows, video production, and broadcast environments requiring real-time editing of 4K, 6K or 8K footage.</p><p>Or for anyone who really, <em>really </em>needs the storage and doesn't mind splurging on it. I've spent a couple of weeks testing and using the LaCie 8big Pro5 and I'm impressed. It makes me feel like I'll never need storage again for generations. Here are three things I like about it and two I don't.</p><h2 class="article-body__section" id="section-what-i-like"><span>What I like</span></h2><h2 id="tank-like-and-hot-swappable-design">Tank-like and hot-swappable 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:6632px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="X3vs2aWDeeF2Xy7haqWW4H" name="Seagate_Lacie-oped-5" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/X3vs2aWDeeF2Xy7haqWW4H-1920-80.jpg" mos="" align="middle" fullscreen="" width="6632" height="3730" 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>When the Seagate LaCie 8big Pro5 arrived at my office and I tried to lift up the cardboard box, I was afraid I was going to pull my back out. That's because the LaCie 8big Pro is built like a tank. This thing is a beast, made of sturdy, solid, single-piece aluminum with a metallic gray finish. I'm certain it could stand the test of time and you'd find it, intact, under the rubble of a demolished building in the post-apocalyptic world.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/QEB3xsnmoJK3qmzJt5JGzG-1920-80.jpg" alt="A Seagate LaCie 8big Pro5 DAS" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/KDNGQ65iPJADLgPRYspdoG-1920-80.jpg" alt="A Seagate LaCie 8big Pro5 DAS" /><figcaption><small role="credit">Nikita Achanta / Tom's Guide</small></figcaption></figure></figure><p>The LaCie 8big Pro5 is a chunky DAS, measuring 11.69 x 9.13 x 8.46 inches and weighing a whopping 31.59lbs. To make moving it around easier, it features an integrated carrying handle on the top. I highly doubt you'd be doing much traveling with it, though. But hey, if you lifted this thing two or three times, you could probably skip arm day at the gym.</p><p>This DAS is hot-swappable, too. Now, as someone who tests the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> day in and out, I could kind of gauge what 'hot-swappable' meant in this context. On a keyboard, it means that you can change the switches by simply popping them out and not having to de- and re-solder them to the PCB. It's a similar concept on the LaCie 8big Pro5. You can essentially remove and replace the eight individual hard drives while the storage system remains powered on and running, and your projects and data transfers keep running on the remaining active drives.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/88HLKKCyZx8L8tQTp22A5Q-1920-80.jpg" alt="Seagate LaCie 8big Pro5 DAS screenshot" /><figcaption><small role="credit">Seagate / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/k4qdFLWv77wcEtMxwQv68Q-1920-80.jpg" alt="Seagate LaCie 8big Pro5 DAS screenshot" /><figcaption><small role="credit">Seagate / Tom's Guide</small></figcaption></figure></figure><p>I also love the LaCie 8big Pro5's Redundancy Protection. When using a fault-tolerant RAID mode, like RAID 5 or RAID 6, the system uses parity data to keep your files safe while a broken drive is missing.</p><p>You needn't worry about manually getting a replacement drive up and running either, as <a href="https://www.seagate.com/support/software/raid-manager/" target="_blank" rel="nofollow">LaCie RAID Manager</a> automatically rebuilds the data array for you. As someone who's a newbie to this stuff, I appreciate that I don't have to wrap my head around a plethora of technical terms — the software does it for me.</p><h2 id="ridiculous-read-and-write-speeds">Ridiculous read and write speeds</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7279px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="sDx59vzk2KZWN9tLQ3hE4H" name="Seagate_Lacie-oped-4" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/sDx59vzk2KZWN9tLQ3hE4H-1920-80.jpg" mos="" align="middle" fullscreen="" width="7279" height="4094" 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>A storage system that's designed to aid creative workflows wouldn't mean much if it didn't boast fast read and write speeds — and thankfully, the Seagate LaCie 8big Pro5 does just that. I ran a disk speed test using CrystalDiskMark and the numbers speak for themselves. Read and write speeds are more dependant on the hard drives themselves, rather than the enclosure, and I tested the LaCie 8big Pro5 with Seagate IronWolf Pro 24TB Enterprise NAS HDDs (<a href="https://www.amazon.com/Seagate-IronWolf-Enterprise-Internal-Drive/dp/B0CSPCFKR9/" target="_blank" rel="nofollow">$859 each</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:1510px;"><p class="vanilla-image-block" style="padding-top:56.29%;"><img id="LxyQHLu6HdA6RbjbjffcAQ" name="Seagate_Lacie_oped-10" alt="CrystalDiskMark screenshot" src="https://cdn.mos.cms.futurecdn.net/LxyQHLu6HdA6RbjbjffcAQ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/LxyQHLu6HdA6RbjbjffcAQ-1920-80.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: CrystalDiskMark / Tom's Guide)</span></figcaption></figure><p>As you can see from the screenshot above, the LaCie 8big Pro5 boasts <strong>2,928MBps read and 1,687MBps write speeds</strong> (Q8T1). These are great for copying movies, video files, or gargantuan RAW images. The <strong>2,387MBps read and 1,572MBps write speeds</strong> in Q1T1 represent standard real-world transfer speeds when copying single large files one at a time.</p><p>These numbers basically tell you that the LaCie 8big Pro5 is healthy and operating at high speeds, and the DAS delivers fast boot times, rapid game load screens, and smooth handling of heavy file transfers. Overall, the LaCie 8big Pro5 performs formidably and proves its worth in complex workflows.</p><h2 id="doubles-as-a-charger">Doubles as a charger!</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4146px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AsDKo6kUR8PPcEqhL5RgLG" name="Seagate_Lacie-oped-2" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/AsDKo6kUR8PPcEqhL5RgLG-1920-80.jpg" mos="" align="middle" fullscreen="" width="4146" height="2332" 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 don't know about you but I really don't like multiple cables cluttering my desk and making my setup look messy. A good cable management solution is a dock, and the Seagate LaCie 8big Pro5's got one built it in — it's a DAS that essentially doubles as a charger.</p><p>Located on the back are four USB-C ports. One of these is a Thunderbolt 5 port with 140W of power delivery, so you can power your Windows laptop or MacBook without an dedicated charger. There are also two downstream ports for connecting external displays (up to two 8K monitors) or daisy-chaining additional Thunderbolt devices with up to 30W PD per port.</p><p>The last is a downstream USB 3.2 Gen 2 port with speeds up to 20Gbps and 15W PD, and it's meant for your peripherals, like mice and keyboards. The 20Gbps speed is excellent as it made quick work of transferring big 4K video files from the <a href="https://www.tomsguide.com/cameras-photography/sony-rx10-v-review">Sony RX10 V</a>. The LaCie 8big Pro5 basically packs everything you could ask for to maintain a neat and tidy setup. </p><h2 class="article-body__section" id="section-what-i-don-t-like"><span>What I don't like</span></h2><h2 id="the-eye-watering-price-tag">The eye-watering price tag</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6870px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Lh3Ku7nHPB25vUym4rAkBH" name="Seagate_Lacie-oped-9" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/Lh3Ku7nHPB25vUym4rAkBH-1920-80.jpg" mos="" align="middle" fullscreen="" width="6870" height="3864" 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>Direct-Attached Storage systems don't come cheap, and the same goes for the Seagate LaCie 8big Pro5. Given that this DAS is designed for creative professionals, film studios, and perhaps game developers, it's no surprise that it costs an arm and a leg. Given that, it would be unfair to mark it down for the premium price tag, but it's worth pointing out anyway.</p><p>The LaCie 8big Pro5 starts at <a href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow">$6,812</a> for the 32TB model and goes up to <a href="https://www.seagate.com/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">$14,259</a> for the 256TB model. I'll give you a moment to take it in. If you want a breakdown of all the models, I've done the homework for you:</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Model</strong></p></th><th  ><p>Price in USD</p></th><th  ><p>Price in GBP</p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>32TB</strong></p></td><td  ><p><a href="https://www.newegg.com/seagate-model-stnc32000400/p/2WA-000W-007D0" target="_blank" rel="nofollow">$6,812</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-32tb-3303665/" target="_blank" rel="nofollow">£6,169</a></p></td></tr><tr><td class="firstcol " ><p><strong>64TB</strong></p></td><td  ><p><a href="https://www.antonline.com/Seagate/Miscellaneous/1562938" target="_blank" rel="nofollow">$8,005</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-64tb-3303666/" target="_blank" rel="nofollow">£7,469</a></p></td></tr><tr><td class="firstcol " ><p><strong>128TB</strong></p></td><td  ><p><a href="https://www.antonline.com/Seagate/Miscellaneous/1562936" target="_blank" rel="nofollow">$9,867</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-128tb-3303667/" target="_blank" rel="nofollow">£7,999</a></p></td></tr><tr><td class="firstcol " ><p><strong>192TB</strong></p></td><td  ><p><a href="https://www.antonline.com/Seagate/Miscellaneous/1562937" target="_blank" rel="nofollow">$12,394</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-192tb-3303668/" target="_blank" rel="nofollow">£11,589</a></p></td></tr><tr><td class="firstcol " ><p><strong>256TB</strong></p></td><td  ><p><a href="https://www.seagate.com/products/creative-pro/lacie-8-big-pro-5/" target="_blank" rel="nofollow">$14,259</a></p></td><td  ><p><a href="https://www.wexphotovideo.com/lacie-8big-pro-5-256tb-3303669/" target="_blank" rel="nofollow">£14,709</a></p></td></tr></tbody></table></div><p>Yep, it's a lot. When I showed this table to my coworker and fellow <a href="https://www.tomsguide.com/uk/author/erin-bashford">senior reviews writer, Erin</a>, she exclaimed over Slack, "You could buy ALMOST FIVE of my cars for that. That's 4.6 2012 Volkswagen Polos for £14,000. And my old car? You could get FORTY SIX 2007 Fiat Pandas for £14K."</p><p>Thankfully, though, should something go wrong with your LaCie 8big Pro5, Seagate happily offers a 5-year warranty and Rescue Data Recovery services (but it isn't available in all countries). </p><h2 id="e-waste-anyone">E-waste, anyone?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6959px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="Z3oVT7V3ezsWhg8Wm9iH6H" name="Seagate_Lacie-oped" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/Z3oVT7V3ezsWhg8Wm9iH6H-1920-80.jpg" mos="" align="middle" fullscreen="" width="6959" height="3914" 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 might be a minor complaint and it might not matter to many people — but it does to me. The Seagate LaCie 8big Pro5 comes with not one, not two, not three, but <em>four </em>power cords for various international standards. I can kind of understand why that is. If you work for a film studio and are constantly lugging hard drives and storage units on planes or other modes of transport, perhaps you'd need — not one, not two, not three — four international power cables.</p><p>I, personally, find it a little frustrating, as most people will end up using just one power cord — as I did the U.K. one in my testing — and will probably get rid of the other three. These will then end up as e-waste in a landfill somewhere. As an environmentally-conscious person who tries to do her part to save the planet, it's annoying. I wish Seagate gave you the option to pick the power cords you needed most at checkout.</p><h2 class="article-body__section" id="section-looking-ahead"><span>Looking ahead...</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:7425px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="zdWBMfSRjGxzuB2JXDWMCH" name="Seagate_Lacie-oped-6" alt="A Seagate LaCie 8big Pro5 DAS" src="https://cdn.mos.cms.futurecdn.net/zdWBMfSRjGxzuB2JXDWMCH-1920-80.jpg" mos="" align="middle" fullscreen="" width="7425" height="4177" 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 you're someone who deals with massive video files and RAW images, and you need massive storage capacity and ultra-fast data speeds, the Seagate LaCie 8big Pro5 is the DAS for you. I've thoroughly enjoyed my time with it. It's powerful and it's built like a tank, and it's hot-swappable design means you can remove the individual SSDs without worrying about data corruption.</p><p>I also love that the LaCie 8big Pro5 essentially doubles as a dock, or a charger, for all your peripherals and external displays, which results in effective cable management. If you've got $7,000 saved up and need a competent DAS, this is the one to get.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xk8gMe"></div>                            </div>                            <script src="https://kwizly.com/embed/Xk8gMe.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/best-picks/best-external-hard-drives">These are the 5 best external SSDs we've tested to free up space on your laptop and phone</a></li><li><a href="https://www.tomsguide.com/computing/the-coolest-piece-of-tech-i-bought-all-year-was-the-dxp4800-plus-heres-why-its-a-game-changer">The coolest piece of tech I bought all year was the DXP4800 Plus — here’s why it’s a game changer</a></li><li><a href="https://www.tomsguide.com/cameras-photography/sony-rx10-v-review">I’ve taken over 3,000 photos on the Sony RX10 V — its 25x zoom lens and 30fps blackout-free burst make it the king of bridge cameras</a></li></ul><div data-widget-type="review" data-model-name="LaCie 8big Pro5"></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Obsbot Meet Flip review: my new tiny work-call companion ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Obsbot Meet Flip is one of the smallest webcams we’ve tested, but is it one of the <a href="https://www.tomsguide.com/computing/peripherals/best-webcams">best webcams</a>? As much as I like it, I’m not sure. </p><p>The camera is highly portable and easy to set up. The 4K quality is ideal for making you look clear, and the companion software lets you make any adjustments you may need. The flip camera lens is also great for privacy and adjusting angles. </p><p>However, I wish the onboard microphone were better. If you want to find out more about the webcam, you can keep reading this full Obsbot Meet Flip review. </p><h2 class="article-body__section" id="section-obsbot-meet-flip-review-cheat-sheet"><span>Obsbot Meet Flip review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A tiny 4K webcam</li><li><strong>Who is it for?</strong> Anyone who needs a small webcam for on-the-go use</li><li><strong>What does it cost? </strong>The Obsbot Meet Flip is available for <a href="https://www.amazon.com/OBSBOT-Webcam-Laptop-Foldable-Sensor/dp/B0H869L7HJ" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/OBSBOT-Meet-Flip-Foldable-Framing/dp/B0GX94B2T6" target="_blank" rel="nofollow">£119</a> from Amazon or <a href="https://www.obsbot.com/store/products/meet-flip" target="_blank" rel="nofollow">£99 from the Obsbot Online store</a></li><li><strong>What do we like?</strong> It's highly portable and easy to set up, delivers a clear image, and has effective companion software</li><li><strong>What don’t we like?</strong> The microphone isn’t the best</li></ul><h2 class="article-body__section" id="section-obsbot-meet-flip-review-specs"><span>Obsbot Meet Flip 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/OBSBOT-Webcam-Laptop-Foldable-Sensor/dp/B0H869L7HJ" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/OBSBOT-Meet-Flip-Foldable-Framing/dp/B0GX94B2T6" target="_blank" rel="nofollow">£119</a></p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (open)</strong></p></td><td  ><p>1.6 x 1.5 x 1.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (closed)</strong></p></td><td  ><p>1.8 x 1.6 x 0.7 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight </strong></p></td><td  ><p>1.3 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Sensor</strong></p></td><td  ><p>1/2"CMOS</p></td></tr><tr><td class="firstcol " ><p><strong>Aperture</strong></p></td><td  ><p>f/1.8</p></td></tr><tr><td class="firstcol " ><p><strong>Max video resolution</strong></p></td><td  ><p>4K/30fps</p></td></tr><tr><td class="firstcol " ><p><strong>Zoom</strong></p></td><td  ><p>4x digital zoom</p></td></tr><tr><td class="firstcol " ><p><strong>Exposure compensation</strong></p></td><td  ><p>±3 EV</p></td></tr><tr><td class="firstcol " ><p><strong>ISO</strong></p></td><td  ><p>100 - 6400</p></td></tr><tr><td class="firstcol " ><p><strong>Audio</strong></p></td><td  ><p>Intelligent Dual-Mic System</p></td></tr><tr><td class="firstcol " ><p><strong>Audio modes</strong></p></td><td  ><p>Directional mode, Dual-directional mode, Smart omni mode, Pure audio mode</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows 10, macOS 11 or later</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-obsbot-meet-flip-review-the-ups"><span>Obsbot Meet Flip review: The ups</span></h2><p>If you’re looking for a tiny, portable webcam, the Obsbot Meet Flip is a great choice. It’s simple to use and comes with companion software to make it your own. </p><h2 id="about-as-small-as-you-can-get">About as small as you can get</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1622px;"><p class="vanilla-image-block" style="padding-top:66.58%;"><img id="PhvEVutJC3umDp2VsVaqcc" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/PhvEVutJC3umDp2VsVaqcc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1622" height="1080" attribution="" endorsement="" class="inline"></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 portability is your main concern when it comes to choosing the perfect webcam, the Obsbot Meet Flip is the one for you. It’s an ideal choice for anyone who needs a camera to go with their laptop, as it’s lightweight and won’t weigh the screen down in the same way as something like the <a href="https://www.tomsguide.com/computing/peripherals/dell-pro-7-webcam-review">Dell Pro 7 Webcam</a> ($199). </p><p>The Obsbot Meet Flip measures 1.8 x 1.6 x 0.7 inches when folded, and 1.6 x 1.5 x 1.3 inches when open. The only webcam we’ve tested that’s smaller is the <a href="https://www.tomsguide.com/computing/peripherals/opal-tadpole-webcam-review-desktop-users-must-keep-kissing-frogs">Opal Tadpole</a> ($129), which is 1.5 x 1.3 x 0.7 inches when operational.</p><p>Having a small webcam like the Meet Flip means you can throw it in your bag and have it ready to go whenever you need it. And because it folds up, the lens won’t get any scratches when it's in your bag. </p><h2 id="couldn-t-be-easier">Couldn’t be easier</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="G3SFKqFofsg49fbb9Xpa2d" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/G3SFKqFofsg49fbb9Xpa2d-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 was no fuss when setting up the Obsbot Meet Flip. The camera comes with a five-foot USB-C cable that plugs into the back of the camera. As soon as this is connected to your computer, it becomes operational. You just have to make sure the camera is selected as your video input for Google Meet, Teams, or whatever video calling software you’re using. </p><h2 id="clear-image">Clear image</h2><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="9ECn8BqSiEXWUUiFLnWZj8" name="Obsbot Meet Flip" alt="pictures taken with the Obsbot Meet Flip" src="https://cdn.mos.cms.futurecdn.net/9ECn8BqSiEXWUUiFLnWZj8-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The first thing I noticed about the Meet Flip was the clear image. The camera shoots in 4K at 30 frames per second, so I looked crisp, and my movements were smooth. </p><p>It’s worth noting that the camera is warm-toned, so if this isn’t for you, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-pro-review">Insta360 Link 2C Pro</a> ($199) for a more natural look. However, I liked the warmth the camera added, as I think it made me look more professional. </p><h2 id="special-effects">Special effects </h2><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="TdkhRTKY8wLHNVYMpdLun8" name="Obsbot Meet Flip" alt="pictures taken with the Obsbot Meet Flip" src="https://cdn.mos.cms.futurecdn.net/TdkhRTKY8wLHNVYMpdLun8-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The Meet Flip is compatible with Obsbot Centre. This downloadable software gives you access to special effects for the webcam, such as backgrounds and color effects. Customization with backgrounds is useful as you can look like you’re in an office for a more professional look, or like you’re at the park.</p><p>When using the software, you can also use hand gestures to use the 4x digital zoom. However, for the framing to be effective, you need to stay within the frame, which I’ll talk about later. </p><h2 id="flip-down-for-privacy">Flip down for privacy</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pg4iNV8jzHjZnZ8d9SGC4d" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/pg4iNV8jzHjZnZ8d9SGC4d-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 to quickly turn off your camera while in a meeting but don’t want to head into your camera settings, you can simply flip the Meet Flip down. Folding the lens away puts the camera into sleep mode and assures complete privacy. </p><p>But that’s not the only thing the Flip is good for. Slightly folding the webcam up and down will guarantee you the perfect camera angle. I found it useful because I like to move around when I’m working to get a change of scenery, but that means my laptop is always placed at different heights. Having the option to flip and fold the camera meant I could make sure I was always in frame and getting my best angle.</p><h2 class="article-body__section" id="section-obsbot-meet-flip-review-the-downs"><span>Obsbot Meet Flip review: The downs</span></h2><p>While I mostly love the Obsbot Meet Flip, the tracking capabilities aren’t as good as other options on the market, and the built-in microphone left me disappointed. </p><h2 id="stay-in-field-of-view-for-framing">Stay in field of view for framing</h2><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="rHAVN2roWhEYPmckiGScp8" name="Obsbot Meet Flip" alt="pictures taken with the Obsbot Meet Flip" src="https://cdn.mos.cms.futurecdn.net/rHAVN2roWhEYPmckiGScp8-1920-80.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: Tom's Guide)</span></figcaption></figure><p>When using the Obsbot Centre software, you can enable auto-tracking. However, this won’t be the best option if you are presenting and moving around. You still need to sit within the field of view of the camera. </p><p>If you want something with <em>actual</em> tracking capabilities, look to the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-pro-review">Insta360 Link 2C Pro</a> ($199). This camera smoothly follows you around the room by cropping the image, making the tracking feel more like a more expensive gimbal-tracking camera. </p><p>The Meet Flip, on the other hand, just uses zoom to follow you around the frame, and this is nowhere near as smooth as the results you get from the Insta360, but you’d expect that since it’s $80 more expensive. </p><h2 id="can-you-hear-me">Can you hear me?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RNYKuN3PPPYr2ModjyTXCd" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/RNYKuN3PPPYr2ModjyTXCd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 audio modes on the Obsbot Meet Flip through the Obsbot Centre, including Directional mode, Dual-directional mode, Smart omni mode and Pure audio mode. It’s great to have options depending on your environment, but I don’t think they worked that well. </p><p>When I took some recordings with the microphone, I couldn’t hear much of a difference between the modes, and I don’t think my voice sounded any clearer between them. I much prefer the way I sound when using the built-in microphone of my <a href="https://www.tomsguide.com/reviews/macbook-pro-13-inch-m2-2022">MacBook Pro M2</a>. That said, the mic was good enough to make sure my voice was audible on work calls.</p><h2 class="article-body__section" id="section-obsbot-meet-flip-review-verdict"><span>Obsbot Meet Flip review: Verdict</span></h2><p>The Obsbot Meet Flip is incredibly small, so it’s easy to throw in your bag and take with you anywhere. The setup of the camera is very simple; you just have to plug in the USB-C cord, and it starts working immediately. </p><p>The 4K image quality is very impressive, and you can see me in detail. But don't worry, if it’s not a good hair or makeup day, you can use the Obsbot Centre companion software to make adjustments. Angle adjustments are easy, since the hinged flip-for-privacy feature also lets you position the camera to your liking.</p><p>However, the tracking on the camera isn’t as smooth as other options on the market, and the microphone performance was a letdown. But for something so tiny and portable, you may think these trade-offs are worth it.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/obsbot-meet-flip-review</link>
                                                                            <description>
                            <![CDATA[ The Obsbot Meet Flip is one of the smallest webcams you can buy and delivers great 4K video, but the microphone may let you down. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">kxTpQr2pC46Q4Ri9F6utN6</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/KSrpwRpB8bM9QHnfYu5nMc-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 27 Aug 2026 13:17:50 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></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-320-70.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/KSrpwRpB8bM9QHnfYu5nMc-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Obsbot Meet Flip foldable webcam photographed in front of a blue background]]></media:description>                                                            <media:text><![CDATA[Obsbot Meet Flip foldable webcam photographed in front of a blue background]]></media:text>
                                <media:title type="plain"><![CDATA[Obsbot Meet Flip foldable webcam photographed in front of a blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/KSrpwRpB8bM9QHnfYu5nMc-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Obsbot Meet Flip is one of the smallest webcams we’ve tested, but is it one of the <a href="https://www.tomsguide.com/computing/peripherals/best-webcams">best webcams</a>? As much as I like it, I’m not sure. </p><p>The camera is highly portable and easy to set up. The 4K quality is ideal for making you look clear, and the companion software lets you make any adjustments you may need. The flip camera lens is also great for privacy and adjusting angles. </p><p>However, I wish the onboard microphone were better. If you want to find out more about the webcam, you can keep reading this full Obsbot Meet Flip review. </p><h2 class="article-body__section" id="section-obsbot-meet-flip-review-cheat-sheet"><span>Obsbot Meet Flip review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A tiny 4K webcam</li><li><strong>Who is it for?</strong> Anyone who needs a small webcam for on-the-go use</li><li><strong>What does it cost? </strong>The Obsbot Meet Flip is available for <a href="https://www.amazon.com/OBSBOT-Webcam-Laptop-Foldable-Sensor/dp/B0H869L7HJ" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/OBSBOT-Meet-Flip-Foldable-Framing/dp/B0GX94B2T6" target="_blank" rel="nofollow">£119</a> from Amazon or <a href="https://www.obsbot.com/store/products/meet-flip" target="_blank" rel="nofollow">£99 from the Obsbot Online store</a></li><li><strong>What do we like?</strong> It's highly portable and easy to set up, delivers a clear image, and has effective companion software</li><li><strong>What don’t we like?</strong> The microphone isn’t the best</li></ul><h2 class="article-body__section" id="section-obsbot-meet-flip-review-specs"><span>Obsbot Meet Flip 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/OBSBOT-Webcam-Laptop-Foldable-Sensor/dp/B0H869L7HJ" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/OBSBOT-Meet-Flip-Foldable-Framing/dp/B0GX94B2T6" target="_blank" rel="nofollow">£119</a></p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (open)</strong></p></td><td  ><p>1.6 x 1.5 x 1.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (closed)</strong></p></td><td  ><p>1.8 x 1.6 x 0.7 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight </strong></p></td><td  ><p>1.3 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Sensor</strong></p></td><td  ><p>1/2"CMOS</p></td></tr><tr><td class="firstcol " ><p><strong>Aperture</strong></p></td><td  ><p>f/1.8</p></td></tr><tr><td class="firstcol " ><p><strong>Max video resolution</strong></p></td><td  ><p>4K/30fps</p></td></tr><tr><td class="firstcol " ><p><strong>Zoom</strong></p></td><td  ><p>4x digital zoom</p></td></tr><tr><td class="firstcol " ><p><strong>Exposure compensation</strong></p></td><td  ><p>±3 EV</p></td></tr><tr><td class="firstcol " ><p><strong>ISO</strong></p></td><td  ><p>100 - 6400</p></td></tr><tr><td class="firstcol " ><p><strong>Audio</strong></p></td><td  ><p>Intelligent Dual-Mic System</p></td></tr><tr><td class="firstcol " ><p><strong>Audio modes</strong></p></td><td  ><p>Directional mode, Dual-directional mode, Smart omni mode, Pure audio mode</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows 10, macOS 11 or later</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-obsbot-meet-flip-review-the-ups"><span>Obsbot Meet Flip review: The ups</span></h2><p>If you’re looking for a tiny, portable webcam, the Obsbot Meet Flip is a great choice. It’s simple to use and comes with companion software to make it your own. </p><h2 id="about-as-small-as-you-can-get">About as small as you can get</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1622px;"><p class="vanilla-image-block" style="padding-top:66.58%;"><img id="PhvEVutJC3umDp2VsVaqcc" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/PhvEVutJC3umDp2VsVaqcc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1622" height="1080" attribution="" endorsement="" class="inline"></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 portability is your main concern when it comes to choosing the perfect webcam, the Obsbot Meet Flip is the one for you. It’s an ideal choice for anyone who needs a camera to go with their laptop, as it’s lightweight and won’t weigh the screen down in the same way as something like the <a href="https://www.tomsguide.com/computing/peripherals/dell-pro-7-webcam-review">Dell Pro 7 Webcam</a> ($199). </p><p>The Obsbot Meet Flip measures 1.8 x 1.6 x 0.7 inches when folded, and 1.6 x 1.5 x 1.3 inches when open. The only webcam we’ve tested that’s smaller is the <a href="https://www.tomsguide.com/computing/peripherals/opal-tadpole-webcam-review-desktop-users-must-keep-kissing-frogs">Opal Tadpole</a> ($129), which is 1.5 x 1.3 x 0.7 inches when operational.</p><p>Having a small webcam like the Meet Flip means you can throw it in your bag and have it ready to go whenever you need it. And because it folds up, the lens won’t get any scratches when it's in your bag. </p><h2 id="couldn-t-be-easier">Couldn’t be easier</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="G3SFKqFofsg49fbb9Xpa2d" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/G3SFKqFofsg49fbb9Xpa2d-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 was no fuss when setting up the Obsbot Meet Flip. The camera comes with a five-foot USB-C cable that plugs into the back of the camera. As soon as this is connected to your computer, it becomes operational. You just have to make sure the camera is selected as your video input for Google Meet, Teams, or whatever video calling software you’re using. </p><h2 id="clear-image">Clear image</h2><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="9ECn8BqSiEXWUUiFLnWZj8" name="Obsbot Meet Flip" alt="pictures taken with the Obsbot Meet Flip" src="https://cdn.mos.cms.futurecdn.net/9ECn8BqSiEXWUUiFLnWZj8-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The first thing I noticed about the Meet Flip was the clear image. The camera shoots in 4K at 30 frames per second, so I looked crisp, and my movements were smooth. </p><p>It’s worth noting that the camera is warm-toned, so if this isn’t for you, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-pro-review">Insta360 Link 2C Pro</a> ($199) for a more natural look. However, I liked the warmth the camera added, as I think it made me look more professional. </p><h2 id="special-effects">Special effects </h2><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="TdkhRTKY8wLHNVYMpdLun8" name="Obsbot Meet Flip" alt="pictures taken with the Obsbot Meet Flip" src="https://cdn.mos.cms.futurecdn.net/TdkhRTKY8wLHNVYMpdLun8-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The Meet Flip is compatible with Obsbot Centre. This downloadable software gives you access to special effects for the webcam, such as backgrounds and color effects. Customization with backgrounds is useful as you can look like you’re in an office for a more professional look, or like you’re at the park.</p><p>When using the software, you can also use hand gestures to use the 4x digital zoom. However, for the framing to be effective, you need to stay within the frame, which I’ll talk about later. </p><h2 id="flip-down-for-privacy">Flip down for privacy</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="pg4iNV8jzHjZnZ8d9SGC4d" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/pg4iNV8jzHjZnZ8d9SGC4d-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 to quickly turn off your camera while in a meeting but don’t want to head into your camera settings, you can simply flip the Meet Flip down. Folding the lens away puts the camera into sleep mode and assures complete privacy. </p><p>But that’s not the only thing the Flip is good for. Slightly folding the webcam up and down will guarantee you the perfect camera angle. I found it useful because I like to move around when I’m working to get a change of scenery, but that means my laptop is always placed at different heights. Having the option to flip and fold the camera meant I could make sure I was always in frame and getting my best angle.</p><h2 class="article-body__section" id="section-obsbot-meet-flip-review-the-downs"><span>Obsbot Meet Flip review: The downs</span></h2><p>While I mostly love the Obsbot Meet Flip, the tracking capabilities aren’t as good as other options on the market, and the built-in microphone left me disappointed. </p><h2 id="stay-in-field-of-view-for-framing">Stay in field of view for framing</h2><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="rHAVN2roWhEYPmckiGScp8" name="Obsbot Meet Flip" alt="pictures taken with the Obsbot Meet Flip" src="https://cdn.mos.cms.futurecdn.net/rHAVN2roWhEYPmckiGScp8-1920-80.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: Tom's Guide)</span></figcaption></figure><p>When using the Obsbot Centre software, you can enable auto-tracking. However, this won’t be the best option if you are presenting and moving around. You still need to sit within the field of view of the camera. </p><p>If you want something with <em>actual</em> tracking capabilities, look to the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-pro-review">Insta360 Link 2C Pro</a> ($199). This camera smoothly follows you around the room by cropping the image, making the tracking feel more like a more expensive gimbal-tracking camera. </p><p>The Meet Flip, on the other hand, just uses zoom to follow you around the frame, and this is nowhere near as smooth as the results you get from the Insta360, but you’d expect that since it’s $80 more expensive. </p><h2 id="can-you-hear-me">Can you hear me?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RNYKuN3PPPYr2ModjyTXCd" name="Obsbot Meet Flip" alt="Obsbot Meet Flip foldable webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/RNYKuN3PPPYr2ModjyTXCd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 audio modes on the Obsbot Meet Flip through the Obsbot Centre, including Directional mode, Dual-directional mode, Smart omni mode and Pure audio mode. It’s great to have options depending on your environment, but I don’t think they worked that well. </p><p>When I took some recordings with the microphone, I couldn’t hear much of a difference between the modes, and I don’t think my voice sounded any clearer between them. I much prefer the way I sound when using the built-in microphone of my <a href="https://www.tomsguide.com/reviews/macbook-pro-13-inch-m2-2022">MacBook Pro M2</a>. That said, the mic was good enough to make sure my voice was audible on work calls.</p><h2 class="article-body__section" id="section-obsbot-meet-flip-review-verdict"><span>Obsbot Meet Flip review: Verdict</span></h2><p>The Obsbot Meet Flip is incredibly small, so it’s easy to throw in your bag and take with you anywhere. The setup of the camera is very simple; you just have to plug in the USB-C cord, and it starts working immediately. </p><p>The 4K image quality is very impressive, and you can see me in detail. But don't worry, if it’s not a good hair or makeup day, you can use the Obsbot Centre companion software to make adjustments. Angle adjustments are easy, since the hinged flip-for-privacy feature also lets you position the camera to your liking.</p><p>However, the tracking on the camera isn’t as smooth as other options on the market, and the microphone performance was a letdown. But for something so tiny and portable, you may think these trade-offs are worth it.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The MCHOSE K99 V2 is a wonderful, thocky keyboard for hardcore typists — and I really wish it didn’t have these flaws ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> can make typing all day long feel like a walk in the park, and that’s exactly what the MCHOSE K99 V2 does. Fitted with responsive linear switches, the K99 V2 is a delight to type on — and its creamy-thocky sound profile is a joy to listen to. The keyboard sports a colorful design, and its proprietary software makes customization a walk in the park. Oh, and it’s got 1,000Hz polling for casual gaming.</p><p>But the K99 V2 isn’t perfect, and I really wish it was. There’s some flex in its body, and there isn’t much variety when it comes to stock switches. My biggest issue, though, is its web-based software which simply refuses to work on macOS.</p><p>Should you still buy this keyboard? Read my full MCHOSE K99 V2 review to find out.</p><h2 class="article-body__section" id="section-mchose-k99-v2-review-specs"><span>MCHOSE K99 V2 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.mchose.store/products/mchose-k99-mechanical-keyboard" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/MCHOSE-Wireless-Gaming-Keyboard-English/dp/B0DXQGQPNK/" target="_blank" rel="nofollow">£78</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Hyacinth or Icy Creamsicle (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>98%, 99 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>6,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>15.71 x 5.91 x 1.85 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>3.73lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Navy Blue, Emerald Green, Glacier Gradient, Sky Blue, Horizon Orange, Mountains Gradient</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-mchose-k99-v2-review-cheat-sheet"><span>MCHOSE K99 V2 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A near-full-size mechanical keyboard with linear switches</li><li><strong>Who is it for? </strong>Hardcore typists and casual gamers</li><li><strong>How much does it cost? </strong>The MCHOSE K99 V2 is available for <a href="https://www.mchose.store/products/mchose-k99-mechanical-keyboard" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/MCHOSE-Wireless-Gaming-Keyboard-English/dp/B0DXQGQPNK/" target="_blank" rel="nofollow">£78</a></li><li><strong>What do we like?</strong> The colorful design, excellent typing experience and sound, 1KHz polling for gaming, and user-friendly companion software</li><li><strong>What don’t we like?</strong> The limited stock switch choice, flexible body, and finicky web-based software</li></ul><h2 class="article-body__section" id="section-mchose-k99-v2-review-the-ups"><span>MCHOSE K99 V2 review: The ups</span></h2><p>The MCHOSE K99 V2 sports a lovely, colorful design, and it’s a joy to type on. Its proprietary software is extremely user-friendly, too.</p><h2 id="lovely-design">Lovely 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="h7ScjhVEi29bbekaRzXFdL" name="MCHOSE_K99_Mech_Keyboard_002.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/h7ScjhVEi29bbekaRzXFdL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>MCHOSE knows how to make a good-looking keyboard, like the <a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">X75 V2</a> ($89), but it’s also no stranger to making boring ones — case in point, the <a href="https://www.tomsguide.com/computing/peripherals/mchose-gx87-review">GX87</a> ($109). Thankfully, the K99 V2 falls into the former category, as it’s a lovely deck. It’s available in a bunch of colors, such as Navy Blue, Emerald Green, Glacier Gradient, etc. The model I tested features a combination of blue, orange, white and steel keycaps, and I think it looks gorgeous.</p><p>Adding to the K99 V2’s charm is its per-key RGB lighting which is customizable via either web-based or downloadable software (more on that shortly). There’s also an anodized mirror plate on the keyboard’s underside with RGB lighting running around it, which, according to MCHOSE, makes the K99 V2 look stylish. The keyboard’s design is practical too, with a slot located along the top to store the 2.4GHz dongle.</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="HjWCQ2eHTY4fW7XpMfaAYL" name="MCHOSE_K99_Mech_Keyboard_013.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/HjWCQ2eHTY4fW7XpMfaAYL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Similar to the GX87, the K99 V2’s box has the words “YOUNG’S CHOICE” (yes, in all caps) inscribed on it. Reading that left me stumped but thankfully, Pete, our senior reviews editor, had already asked MCHOSE what this means, to which the brand clarified: ‘youthful design.’ And I suppose the K99 V2’s design looks like it would appeal to… Gen Z, perhaps? It’s more colorful than the GX87, at least.</p><h2 id="beautiful-for-typing">Beautiful for typing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hoTuWi88rSm487dWTo8YcL" name="MCHOSE_K99_Mech_Keyboard_010.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/hoTuWi88rSm487dWTo8YcL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 my fair share of keyboards — somewhere around the 100 mark, at the time of writing — so I can confidently say that the MCHOSE K99 V2 offers a seriously beautiful typing experience. The K99 V2 comes fitted with pre-lubed Icy Creamsicle switches which I’ve tested before with the X75 V2.</p><p>These linear switches require a moderate 42g of force to activate, and they have a pre-travel distance of 1.3mm (3.5mm total). This means that you don’t need to press them all the way down to register a command, resulting in fast type speeds. The Icy Creamsicles are ever-so-slightly heavier than the 38g Wisterias in my trusty <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139) but the difference is hardly noticeable in real-world use.</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="s7zs55PrLjDryHBNzPTvJK" name="MCHOSE_K99_Mech_Keyboard_007.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/s7zs55PrLjDryHBNzPTvJK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 K99 V2 features a gasket-mount design which ensures structural integrity while reducing vibrations and unwanted metallic pings. It also makes typing feel super comfortable — and even after a full day’s worth of typing on the K99 V2, my fingers didn’t feel tired at all. Combined with six layers of sound-dampening foam and the switches, the K99 V2 makes a lovely, creamy-thocky sound.</p><p>Lastly, the K99 V2 is fitted with double-shot PBT keycaps which are more durable and resistant to wear-and-tear than their ABS counterparts. One thing worth noting, though, is that the keycap profile differs depending on the keyboard color you get. The Navy Blue, Sky Blue and Horizon Orange models have MDA-profile keycaps, while the Mountains Gradient has Cherry-profile ones. MDA keycaps feature a taller, semi-sculpted rake (generally more comfortable) while Cherry ones are more low-profile (usually better for touch-typing). A lot of this comes down to preference, though — you can always swap out keycaps later on.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>MCHOSE K99 V2</p></td><td  ><p>98</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/mchose-unbox-therapy-ut98-review">MCHOSE x Unbox Therapy UT98</a></p></td><td  ><p>98</p></td><td  ><p>92.02%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">MCHOSE X75 V2</a></p></td><td  ><p>94</p></td><td  ><p>95.37%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100 Pro</a></p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.71</p></td><td  ><p>91.2%</p></td></tr></tbody></table></div><p>I put the K99 V2 through its paces by taking a typing test at <a href="http://10fastfingers.com" target="_blank" rel="nofollow">10fastfingers.com</a> as soon as I unboxed the keyboard. Right off the bat, I achieved a speed of 98 words per minute with 91% accuracy. I’m happy any time I get over 90WPM, so I’m very pleased with the K99 V2’s performance. This is a fantastic keyboard for hardcore typists.</p><h2 id="1-000hz-polling-for-gaming-too">1,000Hz polling for gaming, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EBLAC7z2QB9AS4xzA7YcbL" name="MCHOSE_K99_Mech_Keyboard_005.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/EBLAC7z2QB9AS4xzA7YcbL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though it isn’t mentioned on the MCHOSE K99 V2’s product page or box, it actually has a 1,000Hz polling rate (according to its companion software) which is more than sufficient for casual gamers. MCHOSE has since come out with the <a href="https://www.mchose.store/products/mchose-k99-v3-keyboard">K99 V3 ($109) </a>which features 8,000Hz polling over wired and 2.4GHz wireless connections. But like I’ve said in prior reviews, 8KHz polling is overkill for 99% of gamers, and it’s a drain on system resources. The K99 V2’s 1KHz polling should be just fine for you.</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="LyoxzZyA2enoZQCFUD96VL" name="MCHOSE_K99_Mech_Keyboard_009.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/LyoxzZyA2enoZQCFUD96VL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 played <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> and <a href="https://www.tomsguide.com/reviews/forza-horizon-5">Forza Horizon 5</a>, I didn’t notice any input lag or latency issues. I found the switches extremely responsive, and I appreciated the short pre-travel distance as it meant I could simply tap a key to execute an action. The K99 V2 also features N-key rollover which ensures that each keypress is scanned and correctly detected by the keyboard’s hardware, so you can hold down several keys and rest assured that your inputs will be registered. For instance, in Forza Horizon 5, I held down W to maintain full throttle and A to steer hard into a corner, and I was able to tap the spacebar for the emergency brake too.</p><p>Of course, if you <em>still </em>want a mechanical keyboard with 8KHz polling, you can’t go wrong with the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). And if you’re a serious or competitive gamer, it’s worth checking out Hall Effect keyboards like the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a> ($99).</p><h2 id="user-friendly-companion-software">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/davz548QGdHnSNAjNY2nSN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/UNFVAtqviWiAXbBYASaXVN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/6AoKK2rgXs4vLxXs9TjJQN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/i4VcVshGUw6WtirKDS5FAN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure></figure><p>The MCHOSE K99 V2 is accompanied by <a href="https://drive.google.com/file/d/1J4y6qEhg7yq29hKCKK6mDOT_uuecE30j/view" target="_blank" rel="nofollow">M HUB Driver</a>, a user-friendly app that enables you to customize every aspect of the keyboard. It’s available for free on Windows 10 and above, but annoyingly, it isn’t compatible with macOS (even though the K99 V2 itself is).</p><p>Once you’ve downloaded and installed M HUB Driver on a Windows machine, all you need to do is plug in the K99 V2 or connect it over 2.4GHz to customize it. The software sports a clean interface and is extremely straightforward to use. There are no annoying, intrusive pop-ups or ads — like the ones you get on Asus Armoury Crate when customizing the <a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II </a>($99) — which is great. It enables you to customize the K99 V2’s RGB lighting and effects, remap its keys, record macros, and choose between three polling rates (250, 500 and 1,000Hz).</p><h2 id="competitively-priced-2">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bDoVsgJFk6ywxBXvBanunK" name="MCHOSE_K99_Mech_Keyboard_006.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/bDoVsgJFk6ywxBXvBanunK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 MCHOSE K99 V2 offers a beautiful typing experience and boasts decent gaming performance, too, without breaking the bank. In the U.S., you can buy the keyboard directly from MCHOSE for <a href="https://www.mchose.store/products/mchose-k99-mechanical-keyboard" target="_blank" rel="nofollow">$99</a>, and in the U.K., it’s available for <a href="https://www.amazon.co.uk/MCHOSE-Wireless-Gaming-Keyboard-English/dp/B0DXQGQPNK/" target="_blank" rel="nofollow">£78 at Amazon</a>. It’s available at Amazon U.S., yes, but it costs a whopping <a href="https://www.amazon.com/MCHOSE-Wireless-Mechanical-Keyboard-Connection/dp/B0DJR7VHQN/" target="_blank" rel="nofollow">$199</a> — holy markup, Batman!</p><p>We’ve tested some excellent keyboards in the sub-$100 price range, like the <a href="https://www.tomsguide.com/computing/peripherals/royal-kludge-s98-review">Royal Kludge S98</a> ($99), the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-cypher96-keyboard-review">Epomaker Cypher96</a> ($89), and the five-star <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99). Is the K99 V2 better than all of them? Not necessarily, but if you want a mechanical keyboard for typing on all day long, you can’t go wrong with it.</p><h2 class="article-body__section" id="section-mchose-k99-v2-review-the-downs"><span>MCHOSE K99 V2 review: The downs</span></h2><p>Though the MCHOSE K99 V2 is great, it isn’t without its flaws. There’s some flex in its body, there are no tactile stock switches, and the web-based software is finicky on macOS.</p><h2 id="some-flex-in-the-body">Some flex in the body</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wcqXUa7TxvK48ZMZmgRQXL" name="MCHOSE_K99_Mech_Keyboard_008.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/wcqXUa7TxvK48ZMZmgRQXL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Now, this is a common issue I’ve encountered when testing budget-friendly, plastic keyboards — that doesn’t necessarily make it okay, though. The MCHOSE K99 V2 has a fair bit of flex in its body, and it twists when you hold it by its sides. I’ve seen this on the Epomaker Cypher96, the <a href="https://www.tomsguide.com/computing/keyboards/mchose-unbox-therapy-ut98-review">MCHOSE x Unbox Therapy UT98</a> ($139) and the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-ek68-review">Epomaker EK68</a> ($89).</p><p>It’s not the end of the world, but if it’s a dealbreaker for you, the Royal Kludge S98 and the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k10-v2-mechanical-keyboard-review">Keychron K10 V2</a> ($84) are solid alternatives.</p><h2 id="linear-stock-switches-only">Linear stock switches only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AAUWprhuk3TXgVCW3C6ndL" name="MCHOSE_K99_Mech_Keyboard_012.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/AAUWprhuk3TXgVCW3C6ndL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest gripes with the MCHOSE K99 V2 is the limited stock switch choice. At checkout, you can buy either the Hyacinth or the Icy Creamsicle linears. This could be a little disheartening for fans of tactile, clicky or silent switches. The K99 V2 is hot-swappable and compatible with 3- and 5-pin south-facing switches, but you’ll need to buy them separately.</p><p>If you don’t want to purchase them separately and want more variety at checkout, I’d recommend the Keychron V1 Ultra 8K, the Royal Kludge S98 or the<a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review"> Royal Kludge L75</a> ($79). </p><h2 id="issues-with-web-based-software-on-macos">Issues with web-based software on macOS</h2><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="6A6ZcWFqXWE2CUHNurnrDN" name="MCHOSE_K99V2_app-5" alt="MCHOSE M HUB software screenshot" src="https://cdn.mos.cms.futurecdn.net/6A6ZcWFqXWE2CUHNurnrDN-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/6A6ZcWFqXWE2CUHNurnrDN-1920-80.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: MCHOSE / Tom's Guide)</span></figcaption></figure><p>As I mentioned earlier, the MCHOSE K99 V2’s proprietary, downloadable software is extremely user-friendly — but what do you do if you’re a macOS user? You use the <a href="https://www.mchose.com.cn/#/home" target="_blank" rel="nofollow">M HUB Web </a>web-based software, of course… if it works, that is. Unfortunately, I couldn’t get the website to work.</p><p>For some reason, the website noted that the K99 V2 had been disabled, and regardless of what I tried, it wouldn’t work. I tried launching M HUB Web in Google Chrome and Safari on my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a>. No luck. I then tried it on my personal <a href="https://www.tomsguide.com/reviews/macbook-pro-m1">MacBook Pro M1</a>. Still no luck. I even flashed the K99 V2’s firmware using the proprietary software on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a> but no joy.</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="K9n69Wqd5DmJTTMBaMR3LN" name="MCHOSE_K99V2_app-6" alt="MCHOSE M HUB software screenshot" src="https://cdn.mos.cms.futurecdn.net/K9n69Wqd5DmJTTMBaMR3LN-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/K9n69Wqd5DmJTTMBaMR3LN-1920-80.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: MCHOSE / Tom's Guide)</span></figcaption></figure><p>However, the website worked just fine on the Alienware 16X Aurora. Personally, I’ve never encountered such an issue before. For instance, Keychron Launcher, VIA, and NuPhy IO seem to work fine across operating systems, so this is quite disappointing.</p><p>I’ve reached out to a MCHOSE representative to see if there’s a solution to my problems, and I’ll update the review if/when I hear back. If this is a persistent issue, it’s problematic for macOS users with no access to a Windows machine, as they won’t be able to fully customize the K99 V2. You can make some changes via key combinations, though, as per the<a href="https://cdn.shopify.com/s/files/1/0846/5727/5175/files/K99_V2-User_Manual.pdf?v=1745458729" target="_blank" rel="nofollow"> keyboard’s manual</a>.</p><h2 class="article-body__section" id="section-mchose-k99-v2-review-verdict"><span>MCHOSE K99 V2 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="XAMd5sL3q3bMUPd87euKcL" name="MCHOSE_K99_Mech_Keyboard_003.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/XAMd5sL3q3bMUPd87euKcL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mechanical keyboard for typing all day long, the MCHOSE K99 V2 warrants a recommendation. Its stock linear switches are a joy to type on, and they don’t feel fatiguing either. The keyboard packs a 1,000Hz polling rate for casual gaming, too, and its proprietary software makes customization a breeze.</p><p>On top of that, the K99 V2 sounds beautiful with a creamy-thocky sound profile, and it sports a lovely, colorful design. But it’s far from perfect. There’s some flex in the body and you’re confined to only two stock switch sets — both of them are linear, sadly. Web-based software sounds good on paper but in practice, I found it to be finicky on macOS.</p><p>If none of those flaws sound like dealbreakers, then the K99 V2 could be yours for just under $100. At the end of the day, if you need a keyboard that’s simply delightful to work on, the K99 V2 is a hoot.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/mchose-k99-v2-review</link>
                                                                            <description>
                            <![CDATA[ The MCHOSE K99 V2 offers a wonderful typing experience and thocky sound profile, and its proprietary software is user-friendly. But the keyboard has some flaws. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">Sb3Zm7KHtZQgWz6BSXhYUB</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/8kiujng5a79wJa4399NVyK-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 27 Aug 2026 13:16:46 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/8kiujng5a79wJa4399NVyK-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A colorful MCHOSE K99 V2 mechanical keyboard for productivity]]></media:description>                                                            <media:text><![CDATA[A colorful MCHOSE K99 V2 mechanical keyboard for productivity]]></media:text>
                                <media:title type="plain"><![CDATA[A colorful MCHOSE K99 V2 mechanical keyboard for productivity]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/8kiujng5a79wJa4399NVyK-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> can make typing all day long feel like a walk in the park, and that’s exactly what the MCHOSE K99 V2 does. Fitted with responsive linear switches, the K99 V2 is a delight to type on — and its creamy-thocky sound profile is a joy to listen to. The keyboard sports a colorful design, and its proprietary software makes customization a walk in the park. Oh, and it’s got 1,000Hz polling for casual gaming.</p><p>But the K99 V2 isn’t perfect, and I really wish it was. There’s some flex in its body, and there isn’t much variety when it comes to stock switches. My biggest issue, though, is its web-based software which simply refuses to work on macOS.</p><p>Should you still buy this keyboard? Read my full MCHOSE K99 V2 review to find out.</p><h2 class="article-body__section" id="section-mchose-k99-v2-review-specs"><span>MCHOSE K99 V2 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.mchose.store/products/mchose-k99-mechanical-keyboard" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/MCHOSE-Wireless-Gaming-Keyboard-English/dp/B0DXQGQPNK/" target="_blank" rel="nofollow">£78</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Hyacinth or Icy Creamsicle (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>98%, 99 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>6,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>15.71 x 5.91 x 1.85 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>3.73lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Navy Blue, Emerald Green, Glacier Gradient, Sky Blue, Horizon Orange, Mountains Gradient</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-mchose-k99-v2-review-cheat-sheet"><span>MCHOSE K99 V2 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A near-full-size mechanical keyboard with linear switches</li><li><strong>Who is it for? </strong>Hardcore typists and casual gamers</li><li><strong>How much does it cost? </strong>The MCHOSE K99 V2 is available for <a href="https://www.mchose.store/products/mchose-k99-mechanical-keyboard" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/MCHOSE-Wireless-Gaming-Keyboard-English/dp/B0DXQGQPNK/" target="_blank" rel="nofollow">£78</a></li><li><strong>What do we like?</strong> The colorful design, excellent typing experience and sound, 1KHz polling for gaming, and user-friendly companion software</li><li><strong>What don’t we like?</strong> The limited stock switch choice, flexible body, and finicky web-based software</li></ul><h2 class="article-body__section" id="section-mchose-k99-v2-review-the-ups"><span>MCHOSE K99 V2 review: The ups</span></h2><p>The MCHOSE K99 V2 sports a lovely, colorful design, and it’s a joy to type on. Its proprietary software is extremely user-friendly, too.</p><h2 id="lovely-design">Lovely 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="h7ScjhVEi29bbekaRzXFdL" name="MCHOSE_K99_Mech_Keyboard_002.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/h7ScjhVEi29bbekaRzXFdL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>MCHOSE knows how to make a good-looking keyboard, like the <a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">X75 V2</a> ($89), but it’s also no stranger to making boring ones — case in point, the <a href="https://www.tomsguide.com/computing/peripherals/mchose-gx87-review">GX87</a> ($109). Thankfully, the K99 V2 falls into the former category, as it’s a lovely deck. It’s available in a bunch of colors, such as Navy Blue, Emerald Green, Glacier Gradient, etc. The model I tested features a combination of blue, orange, white and steel keycaps, and I think it looks gorgeous.</p><p>Adding to the K99 V2’s charm is its per-key RGB lighting which is customizable via either web-based or downloadable software (more on that shortly). There’s also an anodized mirror plate on the keyboard’s underside with RGB lighting running around it, which, according to MCHOSE, makes the K99 V2 look stylish. The keyboard’s design is practical too, with a slot located along the top to store the 2.4GHz dongle.</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="HjWCQ2eHTY4fW7XpMfaAYL" name="MCHOSE_K99_Mech_Keyboard_013.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/HjWCQ2eHTY4fW7XpMfaAYL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Similar to the GX87, the K99 V2’s box has the words “YOUNG’S CHOICE” (yes, in all caps) inscribed on it. Reading that left me stumped but thankfully, Pete, our senior reviews editor, had already asked MCHOSE what this means, to which the brand clarified: ‘youthful design.’ And I suppose the K99 V2’s design looks like it would appeal to… Gen Z, perhaps? It’s more colorful than the GX87, at least.</p><h2 id="beautiful-for-typing">Beautiful for typing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hoTuWi88rSm487dWTo8YcL" name="MCHOSE_K99_Mech_Keyboard_010.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/hoTuWi88rSm487dWTo8YcL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 my fair share of keyboards — somewhere around the 100 mark, at the time of writing — so I can confidently say that the MCHOSE K99 V2 offers a seriously beautiful typing experience. The K99 V2 comes fitted with pre-lubed Icy Creamsicle switches which I’ve tested before with the X75 V2.</p><p>These linear switches require a moderate 42g of force to activate, and they have a pre-travel distance of 1.3mm (3.5mm total). This means that you don’t need to press them all the way down to register a command, resulting in fast type speeds. The Icy Creamsicles are ever-so-slightly heavier than the 38g Wisterias in my trusty <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139) but the difference is hardly noticeable in real-world use.</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="s7zs55PrLjDryHBNzPTvJK" name="MCHOSE_K99_Mech_Keyboard_007.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/s7zs55PrLjDryHBNzPTvJK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 K99 V2 features a gasket-mount design which ensures structural integrity while reducing vibrations and unwanted metallic pings. It also makes typing feel super comfortable — and even after a full day’s worth of typing on the K99 V2, my fingers didn’t feel tired at all. Combined with six layers of sound-dampening foam and the switches, the K99 V2 makes a lovely, creamy-thocky sound.</p><p>Lastly, the K99 V2 is fitted with double-shot PBT keycaps which are more durable and resistant to wear-and-tear than their ABS counterparts. One thing worth noting, though, is that the keycap profile differs depending on the keyboard color you get. The Navy Blue, Sky Blue and Horizon Orange models have MDA-profile keycaps, while the Mountains Gradient has Cherry-profile ones. MDA keycaps feature a taller, semi-sculpted rake (generally more comfortable) while Cherry ones are more low-profile (usually better for touch-typing). A lot of this comes down to preference, though — you can always swap out keycaps later on.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>MCHOSE K99 V2</p></td><td  ><p>98</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/mchose-unbox-therapy-ut98-review">MCHOSE x Unbox Therapy UT98</a></p></td><td  ><p>98</p></td><td  ><p>92.02%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">MCHOSE X75 V2</a></p></td><td  ><p>94</p></td><td  ><p>95.37%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100 Pro</a></p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.71</p></td><td  ><p>91.2%</p></td></tr></tbody></table></div><p>I put the K99 V2 through its paces by taking a typing test at <a href="http://10fastfingers.com" target="_blank" rel="nofollow">10fastfingers.com</a> as soon as I unboxed the keyboard. Right off the bat, I achieved a speed of 98 words per minute with 91% accuracy. I’m happy any time I get over 90WPM, so I’m very pleased with the K99 V2’s performance. This is a fantastic keyboard for hardcore typists.</p><h2 id="1-000hz-polling-for-gaming-too">1,000Hz polling for gaming, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EBLAC7z2QB9AS4xzA7YcbL" name="MCHOSE_K99_Mech_Keyboard_005.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/EBLAC7z2QB9AS4xzA7YcbL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though it isn’t mentioned on the MCHOSE K99 V2’s product page or box, it actually has a 1,000Hz polling rate (according to its companion software) which is more than sufficient for casual gamers. MCHOSE has since come out with the <a href="https://www.mchose.store/products/mchose-k99-v3-keyboard">K99 V3 ($109) </a>which features 8,000Hz polling over wired and 2.4GHz wireless connections. But like I’ve said in prior reviews, 8KHz polling is overkill for 99% of gamers, and it’s a drain on system resources. The K99 V2’s 1KHz polling should be just fine for you.</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="LyoxzZyA2enoZQCFUD96VL" name="MCHOSE_K99_Mech_Keyboard_009.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/LyoxzZyA2enoZQCFUD96VL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 played <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> and <a href="https://www.tomsguide.com/reviews/forza-horizon-5">Forza Horizon 5</a>, I didn’t notice any input lag or latency issues. I found the switches extremely responsive, and I appreciated the short pre-travel distance as it meant I could simply tap a key to execute an action. The K99 V2 also features N-key rollover which ensures that each keypress is scanned and correctly detected by the keyboard’s hardware, so you can hold down several keys and rest assured that your inputs will be registered. For instance, in Forza Horizon 5, I held down W to maintain full throttle and A to steer hard into a corner, and I was able to tap the spacebar for the emergency brake too.</p><p>Of course, if you <em>still </em>want a mechanical keyboard with 8KHz polling, you can’t go wrong with the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). And if you’re a serious or competitive gamer, it’s worth checking out Hall Effect keyboards like the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a> ($99).</p><h2 id="user-friendly-companion-software">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/davz548QGdHnSNAjNY2nSN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/UNFVAtqviWiAXbBYASaXVN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/6AoKK2rgXs4vLxXs9TjJQN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/i4VcVshGUw6WtirKDS5FAN-1920-80.jpg" alt="MCHOSE M HUB software screenshot" /><figcaption><small role="credit">MCHOSE / Tom's Guide</small></figcaption></figure></figure><p>The MCHOSE K99 V2 is accompanied by <a href="https://drive.google.com/file/d/1J4y6qEhg7yq29hKCKK6mDOT_uuecE30j/view" target="_blank" rel="nofollow">M HUB Driver</a>, a user-friendly app that enables you to customize every aspect of the keyboard. It’s available for free on Windows 10 and above, but annoyingly, it isn’t compatible with macOS (even though the K99 V2 itself is).</p><p>Once you’ve downloaded and installed M HUB Driver on a Windows machine, all you need to do is plug in the K99 V2 or connect it over 2.4GHz to customize it. The software sports a clean interface and is extremely straightforward to use. There are no annoying, intrusive pop-ups or ads — like the ones you get on Asus Armoury Crate when customizing the <a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II </a>($99) — which is great. It enables you to customize the K99 V2’s RGB lighting and effects, remap its keys, record macros, and choose between three polling rates (250, 500 and 1,000Hz).</p><h2 id="competitively-priced-2">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="bDoVsgJFk6ywxBXvBanunK" name="MCHOSE_K99_Mech_Keyboard_006.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/bDoVsgJFk6ywxBXvBanunK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 MCHOSE K99 V2 offers a beautiful typing experience and boasts decent gaming performance, too, without breaking the bank. In the U.S., you can buy the keyboard directly from MCHOSE for <a href="https://www.mchose.store/products/mchose-k99-mechanical-keyboard" target="_blank" rel="nofollow">$99</a>, and in the U.K., it’s available for <a href="https://www.amazon.co.uk/MCHOSE-Wireless-Gaming-Keyboard-English/dp/B0DXQGQPNK/" target="_blank" rel="nofollow">£78 at Amazon</a>. It’s available at Amazon U.S., yes, but it costs a whopping <a href="https://www.amazon.com/MCHOSE-Wireless-Mechanical-Keyboard-Connection/dp/B0DJR7VHQN/" target="_blank" rel="nofollow">$199</a> — holy markup, Batman!</p><p>We’ve tested some excellent keyboards in the sub-$100 price range, like the <a href="https://www.tomsguide.com/computing/peripherals/royal-kludge-s98-review">Royal Kludge S98</a> ($99), the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-cypher96-keyboard-review">Epomaker Cypher96</a> ($89), and the five-star <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99). Is the K99 V2 better than all of them? Not necessarily, but if you want a mechanical keyboard for typing on all day long, you can’t go wrong with it.</p><h2 class="article-body__section" id="section-mchose-k99-v2-review-the-downs"><span>MCHOSE K99 V2 review: The downs</span></h2><p>Though the MCHOSE K99 V2 is great, it isn’t without its flaws. There’s some flex in its body, there are no tactile stock switches, and the web-based software is finicky on macOS.</p><h2 id="some-flex-in-the-body">Some flex in the body</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wcqXUa7TxvK48ZMZmgRQXL" name="MCHOSE_K99_Mech_Keyboard_008.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/wcqXUa7TxvK48ZMZmgRQXL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Now, this is a common issue I’ve encountered when testing budget-friendly, plastic keyboards — that doesn’t necessarily make it okay, though. The MCHOSE K99 V2 has a fair bit of flex in its body, and it twists when you hold it by its sides. I’ve seen this on the Epomaker Cypher96, the <a href="https://www.tomsguide.com/computing/keyboards/mchose-unbox-therapy-ut98-review">MCHOSE x Unbox Therapy UT98</a> ($139) and the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-ek68-review">Epomaker EK68</a> ($89).</p><p>It’s not the end of the world, but if it’s a dealbreaker for you, the Royal Kludge S98 and the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k10-v2-mechanical-keyboard-review">Keychron K10 V2</a> ($84) are solid alternatives.</p><h2 id="linear-stock-switches-only">Linear stock switches only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AAUWprhuk3TXgVCW3C6ndL" name="MCHOSE_K99_Mech_Keyboard_012.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/AAUWprhuk3TXgVCW3C6ndL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest gripes with the MCHOSE K99 V2 is the limited stock switch choice. At checkout, you can buy either the Hyacinth or the Icy Creamsicle linears. This could be a little disheartening for fans of tactile, clicky or silent switches. The K99 V2 is hot-swappable and compatible with 3- and 5-pin south-facing switches, but you’ll need to buy them separately.</p><p>If you don’t want to purchase them separately and want more variety at checkout, I’d recommend the Keychron V1 Ultra 8K, the Royal Kludge S98 or the<a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review"> Royal Kludge L75</a> ($79). </p><h2 id="issues-with-web-based-software-on-macos">Issues with web-based software on macOS</h2><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="6A6ZcWFqXWE2CUHNurnrDN" name="MCHOSE_K99V2_app-5" alt="MCHOSE M HUB software screenshot" src="https://cdn.mos.cms.futurecdn.net/6A6ZcWFqXWE2CUHNurnrDN-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/6A6ZcWFqXWE2CUHNurnrDN-1920-80.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: MCHOSE / Tom's Guide)</span></figcaption></figure><p>As I mentioned earlier, the MCHOSE K99 V2’s proprietary, downloadable software is extremely user-friendly — but what do you do if you’re a macOS user? You use the <a href="https://www.mchose.com.cn/#/home" target="_blank" rel="nofollow">M HUB Web </a>web-based software, of course… if it works, that is. Unfortunately, I couldn’t get the website to work.</p><p>For some reason, the website noted that the K99 V2 had been disabled, and regardless of what I tried, it wouldn’t work. I tried launching M HUB Web in Google Chrome and Safari on my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a>. No luck. I then tried it on my personal <a href="https://www.tomsguide.com/reviews/macbook-pro-m1">MacBook Pro M1</a>. Still no luck. I even flashed the K99 V2’s firmware using the proprietary software on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a> but no joy.</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="K9n69Wqd5DmJTTMBaMR3LN" name="MCHOSE_K99V2_app-6" alt="MCHOSE M HUB software screenshot" src="https://cdn.mos.cms.futurecdn.net/K9n69Wqd5DmJTTMBaMR3LN-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/K9n69Wqd5DmJTTMBaMR3LN-1920-80.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: MCHOSE / Tom's Guide)</span></figcaption></figure><p>However, the website worked just fine on the Alienware 16X Aurora. Personally, I’ve never encountered such an issue before. For instance, Keychron Launcher, VIA, and NuPhy IO seem to work fine across operating systems, so this is quite disappointing.</p><p>I’ve reached out to a MCHOSE representative to see if there’s a solution to my problems, and I’ll update the review if/when I hear back. If this is a persistent issue, it’s problematic for macOS users with no access to a Windows machine, as they won’t be able to fully customize the K99 V2. You can make some changes via key combinations, though, as per the<a href="https://cdn.shopify.com/s/files/1/0846/5727/5175/files/K99_V2-User_Manual.pdf?v=1745458729" target="_blank" rel="nofollow"> keyboard’s manual</a>.</p><h2 class="article-body__section" id="section-mchose-k99-v2-review-verdict"><span>MCHOSE K99 V2 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="XAMd5sL3q3bMUPd87euKcL" name="MCHOSE_K99_Mech_Keyboard_003.JPG" alt="A colorful MCHOSE K99 V2 mechanical keyboard for productivity" src="https://cdn.mos.cms.futurecdn.net/XAMd5sL3q3bMUPd87euKcL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mechanical keyboard for typing all day long, the MCHOSE K99 V2 warrants a recommendation. Its stock linear switches are a joy to type on, and they don’t feel fatiguing either. The keyboard packs a 1,000Hz polling rate for casual gaming, too, and its proprietary software makes customization a breeze.</p><p>On top of that, the K99 V2 sounds beautiful with a creamy-thocky sound profile, and it sports a lovely, colorful design. But it’s far from perfect. There’s some flex in the body and you’re confined to only two stock switch sets — both of them are linear, sadly. Web-based software sounds good on paper but in practice, I found it to be finicky on macOS.</p><p>If none of those flaws sound like dealbreakers, then the K99 V2 could be yours for just under $100. At the end of the day, if you need a keyboard that’s simply delightful to work on, the K99 V2 is a hoot.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just tested the Turtle Beach KB7 TKL — and 8,000Hz polling, adjustable actuation and a massive touchscreen (almost) make it a winner ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Turtle Beach has finally made screens feel useful without being gimmicky, as the large touchscreen on its Command Series KB7 TKL is simply beautiful. Enabling you to customize actuation points and enable features like Rapid Trigger on the fly, the KB7 TKL is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> you can buy today — if you’ve got nearly $200 to spare.</p><p>The KB7 TKL is not only great for gaming, but it’s solid for typing and productivity, too, thanks to the multi-functional screen and an optional number pad accessory. But the keyboard isn’t perfect, as it isn’t hot-swappable or wireless, and it costs a pretty penny.</p><p>For the complete breakdown, read my full Turtle Beach Command Series KB7 TKL review.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-specs"><span>Turtle Beach Command Series KB7 TKL 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/Turtle-Beach-Command-Hall-Effect-Keyboard/dp/B0GWD2D7S3/" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keyboard/dp/B0GXZN2JNK/" target="_blank" rel="nofollow">£159</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Titan Hall Effect</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 78 keys + 4.3-inch touchscreen</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>1</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>15.71 x 10.08 x 1.22 inches (with palm rest)</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.47lbs (with palm rest)</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-turtle-beach-command-series-kb7-tkl-review-cheat-sheet"><span>Turtle Beach Command Series KB7 TKL review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A wired gaming keyboard with a 75% layout and Hall Effect switches</li><li><strong>Who is it for? </strong>Competitive gamers</li><li><strong>How much does it cost? </strong>The Turtle Beach Command Series KB7 TKL is available for <a href="https://www.amazon.com/Turtle-Beach-Command-Hall-Effect-Keyboard/dp/B0GWD2D7S3/" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keyboard/dp/B0GXZN2JNK/" target="_blank" rel="nofollow">£159</a></li><li><strong>What do we like? </strong>The sturdy build, large screen for on-the-fly customization, elite gaming features, and the decent typing experience</li><li><strong>What don’t we like? </strong>The wired-only, non-hot-swappable design, and expensive price tag</li></ul><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-the-ups"><span>Turtle Beach Command Series KB7 TKL review: The ups</span></h2><p>The Turtle Beach Command Series KB7 TKL nails the sturdy, durable design, and packs a large touchscreen for on-the-fly customization, 8,000Hz polling, Rapid Trigger, and more.</p><h2 id="sturdy-build">Sturdy build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QFFVcSrLbkKhka6uyx4SK5" name="Turtlebeach_KB7_ 21.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/QFFVcSrLbkKhka6uyx4SK5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB7 TKL feels like a supremely well-built keyboard, utilizing a combination of an anodized aluminum top plate and a plastic chassis. These give it some heft, as the keyboard weighs 2.47lbs (including wrist rest) but it’s nowhere near as heavy as some of Keychron’s full-metal decks, like the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review">Q6 HE 8K</a> (5lbs, $239) or the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q12-he-review">Q12 HE</a> (4.85lbs, $249).</p><p>That’s because the KB7 TKL is a low-profile keyboard, and these are generally lighter than standard-profile ones. Though the KB7 TKL is heavier than, say, the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> (1.67lbs, $129) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> (1.65lbs, $139), I haven’t had any issues carrying it to and from work in my backpack.</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="JGfLem9EhZTU4pXwQ2WZi4" name="Turtlebeach_KB7_ 19.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/JGfLem9EhZTU4pXwQ2WZi4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 KB7 TKL, as its name suggests, is a TKL keyboard, so it doesn’t feature a number pad. It measures 15.71 x 10.08 x 1.22 inches so it shouldn’t take up too much space on your desk. If you wish it had a numpad, you’re in luck. You can grab the optional Turtle Beach Command Series KP7 for <a href="https://www.amazon.com/Turtle-Beach-Command-Compact-Hall-Effect/dp/B0GWD7XJB8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keypad/dp/B0GWD7XJB8/" target="_blank" rel="nofollow">£89</a> which conveniently slides into place on either side of the keyboard, thanks to dual modular rails, and turns the KB7 TKL into a full-size keyboard. This is a great choice for both left- and right-handed folks. The KP7 functions as a standalone wired Hall Effect gaming keypad, too.</p><p>Adding to the KB7 TKL’s durable build are its double-shot PBT keycaps which are more durable and resistant to wear and tear than their ABS counterparts. Turtle Beach happily includes a detachable, light-diffusing silicon wrist rest in the box, too, which helps you feel comfortable during long gaming or typing sessions.</p><h2 id="large-touchscreen-for-on-the-fly-customization">Large touchscreen for on-the-fly customization</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Ay64Vt6LhJquRcTG9dczS5" name="Turtlebeach_KB7_ 13.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/Ay64Vt6LhJquRcTG9dczS5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Let’s talk about the elephant in the room: the Turtle Beach Command Series KB7 TKL’s large 4.3-inch touchscreen that takes up <em>a lot </em>of space on the keyboard’s right side. This replaces the navigation keys cluster you’d normally find on other TKL keyboards — but you still get access to these via the touchscreen.</p><p>In addition to that, the KB7 TKL’s touchscreen enables you to adjust all the keys’ sensitivity at once, enable ReacTap and Rapid Trigger, record macros on the go, remap keys, adjust the keyboard’s lighting, and more.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/xMggsJHqhiyEqnmjtAjHuB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Y6zinr3Chk4YC6Z4vfFUWB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/z8ZTkhK6FEFK8MbqALMYeB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Ya5yaBhMJWvXyx7zs5ydjB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/N4AHC7qyvQsFy5p2NiHfhB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Bb6LziHGxR7Cszh3A6CfqB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure></figure><p>To unlock the touchscreen’s full potential, you should download Turtle Beach’s Swarm II companion app on your Windows laptop. Via Swarm II, you can customize the touchscreen by adding widgets to it, or selecting a template preset, such as ‘KB7 CS.’ This is designed specifically for Counter-Strike, and it shows you handy information, such as your equipped weapon, bullets remaining, total kills, etc. You can add third-party apps to the touchscreen, too, like Discord, Steam, and StreamLabs. <em>This </em>is how you make a keyboard’s screen useful and not just a gimmick.</p><h2 id="top-tier-gaming-features">Top-tier gaming 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="T3haF4zGRKD5N3FxWHktS5" name="Turtlebeach_KB7_ 14.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/T3haF4zGRKD5N3FxWHktS5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB7 TKL is, first and foremost, a gaming keyboard. It’s fitted with in-house Titan low-profile Hall Effect switches, and it has an 8,000Hz polling rate. For 99% of gamers, 1,000Hz polling is more than enough. 8KHz is also a drain on system resources, but thankfully, you can drop the polling rate by launching Swarm II on your machine.</p><p>I tested the KB7 TKL by playing Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a>, and the keyboard didn’t let me down even once. I tried both 8KHz and 1KHz polling rates and didn’t notice any input lag or unresponsiveness. The Titan switches’ per-key adjustable actuation is the real star here, though. Their sensitivity can be adjusted between 0.1mm and 3.2mm.</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="Q7TNhdJQ89vY9NxesyvVV5" name="Turtlebeach_KB7_ 18.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/Q7TNhdJQ89vY9NxesyvVV5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 appreciated being able to adjust the sensitivity via the touchscreen itself, so I could quickly drop to a low actuation point of around 0.3mm for ultra-responsive gameplay in Counter-Strike 2. Turning the sensitivity of action and movement keys down meant that I could quickly swap between weapons by lightly tapping a key. Executing a series of complex actions felt like a walk in the park too, as I could sprint, throw a grenade, and crouch into hiding rapidly. This gave me a competitive advantage that often mechanical keyboards can’t offer.</p><p>In addition to that, you also get ReacTap, which is Turtle Beach’s term for Last Key Prioritization — a type of Simultaneous Opposite Key Detection (SOCD) resolution, or in other words, telling the keyboard what to do when two opposing commands are entered. </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:7215px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="fo2FYC5iYgYsDmbHJ7m4k6" name="TB_CSKB7TKL" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/fo2FYC5iYgYsDmbHJ7m4k6-1920-80.jpg" mos="" align="middle" fullscreen="" width="7215" height="4058" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Enabling this essentially prioritizes the last key pressed, and I use it primarily for counter-strafing. You can basically draw to a complete stop from a sideways strafe by pressing the opposing direction key (A and D or vice versa). This gets you kicked out of Valve’s games, like Counter-Strike 2 and DOTA, as it acts as a cheat code — but it’s not banned in Valorant and other non-Valve competitive games. It also works like a charm in single-player games, like Cyberpunk 2077 where I was running circles around NPCs.</p><p>Finally, there’s Rapid Trigger which ensures that all inputs are recognized by enabling near-instantaneous switch reset when lifting. This allows for quicker directional changes and micro-adjustments when dodging or repositioning, for example. In Cyberpunk 2077, I used it to quickly peek around corners. All of these elite features come together to deliver an exceptional gaming experience.</p><h2 id="decent-for-typing-too">Decent for typing, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6245px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hXKK3zJrMq4paVGgQ3V6e6" name="TB_CSKB7TKL-2" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/hXKK3zJrMq4paVGgQ3V6e6-1920-80.jpg" mos="" align="middle" fullscreen="" width="6245" height="3513" attribution="" endorsement="" class="inline"></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 I prefer one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a>, like my trusty <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139), for typing, the Turtle Beach Command Series KB7 TKL is a good deck for productivity, too. The first page of its touchscreen features functions like Print, Insert, Page Up and Page Down, which are more productivity-oriented. There are Copy and Paste shortcuts, which negate the need to hold down Cmd/Ctrl + C/V.</p><p>The KB7 TKL feels good to type on, too. To put it through its paces, I took a series of typing tests at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, with the keyboard set to different sensitives for each test.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Turtle Beach KB7 TKL (3.2mm actuation)</p></td><td  ><p>84</p></td><td  ><p>86%</p></td></tr><tr><td class="firstcol " ><p>Turtle Beach KB7 TKL (1.5mm)</p></td><td  ><p>89</p></td><td  ><p>90.1%</p></td></tr><tr><td class="firstcol " ><p>Turtle Beach KB7 TKL (0.5mm)</p></td><td  ><p>92</p></td><td  ><p>92%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/gravastar-mercury-v75-pro-he-review">Gravastar Mercury V75 Pro HE</a> (0.5mm)</p></td><td  ><p>101</p></td><td  ><p>89.01%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review">Keychron Q6 HE 8K</a> (0.5mm actuation)</p></td><td  ><p>106</p></td><td  ><p>89.02%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-q12-he-review">Keychron Q12 HE</a> (0.5mm actuation)</p></td><td  ><p>103</p></td><td  ><p>87.14%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> (0.5mm actuation)</p></td><td  ><p>102</p></td><td  ><p>93.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/nzxt-function-elite-mini-tkl-review">NZXT Function Elite MiniTKL</a> (0.6mm actuation)</p></td><td  ><p>89</p></td><td  ><p>88.42%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> (0.5mm actuation)</p></td><td  ><p>101</p></td><td  ><p>89.41%</p></td></tr></tbody></table></div><p>As you can see in the table above, the KB7 TKL performs well, but it doesn’t quite match up to other magnetic keyboards I’ve tested. That’s on me, though, as at the time of writing, I’d tested a bunch of mechanical keyboards leading up to the KB7 TKL, and was more accustomed to their tactile feel. I’ve personally enjoyed typing on the KB7 TKL. Its keycaps feel soft and polished to touch, and with the actuation set just right, my fingers felt like they were gliding across the board.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-the-downs"><span>Turtle Beach Command Series KB7 TKL review: The downs</span></h2><p>Annoyingly, the Turtle Beach Command Series KB7 TKL isn’t hot-swappable and can’t be used wirelessly, and it’s expensive, too.</p><h2 id="not-hot-swappable-2">Not hot-swappable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ojVrYWrqGMe95pkQPCvNU5" name="Turtlebeach_KB7_ 17.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/ojVrYWrqGMe95pkQPCvNU5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest drawbacks of the Turtle Beach Command Series KB7 TKL is that it isn’t hot-swappable. That means its Titan Hall Effect switches are soldered to the PCB, so you can’t just pop them out and replace them with switches of your choice. Having tested nearly 100 keyboards at the time of writing, that’s a fairly significant drawback, especially when Turtle Beach’s competitors have been doing it for a while now.</p><p>If you want a hot-swappable keyboard, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review">Keychron K2 HE</a> ($130) or the <a href="https://www.tomsguide.com/computing/peripherals/wooting-80he-review">Wooting 80HE</a> ($199). At the time of writing, Hall Effect switch choice is fairly limited, but we expect more switches to pop up in the market as these keyboards gain more popularity and become more mainstream.</p><h2 id="wired-only-2">Wired-only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vfDkUdE82peZCzVJK3B3M5" name="Turtlebeach_KB7_ 15.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/vfDkUdE82peZCzVJK3B3M5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 more of a pre-purchase consideration rather than an outright flaw: the Turtle Beach Command Series KB7 TKL is a wired-only keyboard. You can connect the keyboard to your machine either via USB-C or two USB-A ports (using the included adapter), which is similar to the <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Turtle Beach Vulcan II Max</a> ($229). You also get USB-A passthrough for connecting other devices straight to the KB7 TKL.</p><p>If you want to use a Hall Effect keyboard on the go and would like wireless connectivity, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> ($99) or the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag </a>($99).</p><h2 id="expensive">Expensive</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="68kynPamz7GBQNLuTx7cS5" name="Turtlebeach_KB7_ 20.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/68kynPamz7GBQNLuTx7cS5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Turtle Beach makes some great peripherals but they don’t always come cheap — and the same goes for the KB7 TKL. The keyboard retails for <a href="https://www.amazon.com/Turtle-Beach-Command-Hall-Effect-Keyboard/dp/B0GWD2D7S3/" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keyboard/dp/B0GXZN2JNK/" target="_blank" rel="nofollow">£159 at Amazon</a> so while it isn’t as expensive as the Keychron Q12 HE ($249) or the <a href="https://www.tomsguide.com/computing/peripherals/lemokey-l5-he-review">Lemokey L5 HE</a> ($220), it isn’t the cheapest either. That price tag hurts more when you take into consideration that the KB7 TKL isn’t hot-swappable and doesn’t offer wireless connectivity.</p><p>The KB7 TKL faces fierce competition from other Hall Effect keyboard manufacturers — and you don’t need to spend nearly $200 or more to get a decent one nowadays. The Keychron K2 HE ($130), the Epomaker HE65 Mag ($99) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air60-he-tiny-profile-huge-performance">NuPhy Air60 HE </a>($139) are exceptional magnetic keyboards that don’t break the bank.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-verdict"><span>Turtle Beach Command Series KB7 TKL 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="JGs9D56WpJFASoXQQ7p5G5" name="Turtlebeach_KB7_ 3.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/JGs9D56WpJFASoXQQ7p5G5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 powerful gaming keyboard, the Turtle Beach Command Series KB7 TKL is a standout choice as the keyboard doesn’t give you too much to complain about. It packs 8,000Hz polling, adjustable actuation, and top-tier gaming features like Rapid Trigger and ReacTap. In addition to that, you get a large customizable touchscreen that enables you to adjust settings on the go.</p><p>But unfortunately, the KB7 TKL has its flaws. It isn’t hot-swappable, which most of its competitors are, and it isn’t wireless either. It’s also expensive and is simply outdone by the likes of Epomaker and Keychron offering better value for money.</p><p>If you aren’t bothered by those drawbacks, then the KB7 TKL could be perfect for you. I, for one, absolutely love how helpful the touchscreen is, and now I wish all keyboards had it.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/turtle-beach-command-series-kb7-tkl-review</link>
                                                                            <description>
                            <![CDATA[ The Turtle Beach Command Series KB7 TKL is a powerful gaming keyboard with 8,000Hz polling and adjustable actuation, and features a customizable touchscreen. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">gdaSUbDWb9wH5T8tNqqxaS</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/HSocuWtzxcJEFd6F5DU8W5-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 27 Aug 2026 13:15:08 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/HSocuWtzxcJEFd6F5DU8W5-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Turtle Beach Command Series KB7 TKL Hall Effect keyboard]]></media:description>                                                            <media:text><![CDATA[A Turtle Beach Command Series KB7 TKL Hall Effect keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A Turtle Beach Command Series KB7 TKL Hall Effect keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/HSocuWtzxcJEFd6F5DU8W5-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Turtle Beach has finally made screens feel useful without being gimmicky, as the large touchscreen on its Command Series KB7 TKL is simply beautiful. Enabling you to customize actuation points and enable features like Rapid Trigger on the fly, the KB7 TKL is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> you can buy today — if you’ve got nearly $200 to spare.</p><p>The KB7 TKL is not only great for gaming, but it’s solid for typing and productivity, too, thanks to the multi-functional screen and an optional number pad accessory. But the keyboard isn’t perfect, as it isn’t hot-swappable or wireless, and it costs a pretty penny.</p><p>For the complete breakdown, read my full Turtle Beach Command Series KB7 TKL review.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-specs"><span>Turtle Beach Command Series KB7 TKL 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/Turtle-Beach-Command-Hall-Effect-Keyboard/dp/B0GWD2D7S3/" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keyboard/dp/B0GXZN2JNK/" target="_blank" rel="nofollow">£159</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Titan Hall Effect</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 78 keys + 4.3-inch touchscreen</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>1</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>15.71 x 10.08 x 1.22 inches (with palm rest)</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.47lbs (with palm rest)</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-turtle-beach-command-series-kb7-tkl-review-cheat-sheet"><span>Turtle Beach Command Series KB7 TKL review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A wired gaming keyboard with a 75% layout and Hall Effect switches</li><li><strong>Who is it for? </strong>Competitive gamers</li><li><strong>How much does it cost? </strong>The Turtle Beach Command Series KB7 TKL is available for <a href="https://www.amazon.com/Turtle-Beach-Command-Hall-Effect-Keyboard/dp/B0GWD2D7S3/" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keyboard/dp/B0GXZN2JNK/" target="_blank" rel="nofollow">£159</a></li><li><strong>What do we like? </strong>The sturdy build, large screen for on-the-fly customization, elite gaming features, and the decent typing experience</li><li><strong>What don’t we like? </strong>The wired-only, non-hot-swappable design, and expensive price tag</li></ul><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-the-ups"><span>Turtle Beach Command Series KB7 TKL review: The ups</span></h2><p>The Turtle Beach Command Series KB7 TKL nails the sturdy, durable design, and packs a large touchscreen for on-the-fly customization, 8,000Hz polling, Rapid Trigger, and more.</p><h2 id="sturdy-build">Sturdy build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QFFVcSrLbkKhka6uyx4SK5" name="Turtlebeach_KB7_ 21.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/QFFVcSrLbkKhka6uyx4SK5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB7 TKL feels like a supremely well-built keyboard, utilizing a combination of an anodized aluminum top plate and a plastic chassis. These give it some heft, as the keyboard weighs 2.47lbs (including wrist rest) but it’s nowhere near as heavy as some of Keychron’s full-metal decks, like the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review">Q6 HE 8K</a> (5lbs, $239) or the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q12-he-review">Q12 HE</a> (4.85lbs, $249).</p><p>That’s because the KB7 TKL is a low-profile keyboard, and these are generally lighter than standard-profile ones. Though the KB7 TKL is heavier than, say, the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> (1.67lbs, $129) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> (1.65lbs, $139), I haven’t had any issues carrying it to and from work in my backpack.</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="JGfLem9EhZTU4pXwQ2WZi4" name="Turtlebeach_KB7_ 19.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/JGfLem9EhZTU4pXwQ2WZi4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 KB7 TKL, as its name suggests, is a TKL keyboard, so it doesn’t feature a number pad. It measures 15.71 x 10.08 x 1.22 inches so it shouldn’t take up too much space on your desk. If you wish it had a numpad, you’re in luck. You can grab the optional Turtle Beach Command Series KP7 for <a href="https://www.amazon.com/Turtle-Beach-Command-Compact-Hall-Effect/dp/B0GWD7XJB8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keypad/dp/B0GWD7XJB8/" target="_blank" rel="nofollow">£89</a> which conveniently slides into place on either side of the keyboard, thanks to dual modular rails, and turns the KB7 TKL into a full-size keyboard. This is a great choice for both left- and right-handed folks. The KP7 functions as a standalone wired Hall Effect gaming keypad, too.</p><p>Adding to the KB7 TKL’s durable build are its double-shot PBT keycaps which are more durable and resistant to wear and tear than their ABS counterparts. Turtle Beach happily includes a detachable, light-diffusing silicon wrist rest in the box, too, which helps you feel comfortable during long gaming or typing sessions.</p><h2 id="large-touchscreen-for-on-the-fly-customization">Large touchscreen for on-the-fly customization</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Ay64Vt6LhJquRcTG9dczS5" name="Turtlebeach_KB7_ 13.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/Ay64Vt6LhJquRcTG9dczS5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Let’s talk about the elephant in the room: the Turtle Beach Command Series KB7 TKL’s large 4.3-inch touchscreen that takes up <em>a lot </em>of space on the keyboard’s right side. This replaces the navigation keys cluster you’d normally find on other TKL keyboards — but you still get access to these via the touchscreen.</p><p>In addition to that, the KB7 TKL’s touchscreen enables you to adjust all the keys’ sensitivity at once, enable ReacTap and Rapid Trigger, record macros on the go, remap keys, adjust the keyboard’s lighting, and more.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/xMggsJHqhiyEqnmjtAjHuB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Y6zinr3Chk4YC6Z4vfFUWB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/z8ZTkhK6FEFK8MbqALMYeB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Ya5yaBhMJWvXyx7zs5ydjB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/N4AHC7qyvQsFy5p2NiHfhB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Bb6LziHGxR7Cszh3A6CfqB-1920-80.jpg" alt="Turtle Beach Swarm II screenshot" /><figcaption><small role="credit">Turtle Beach / Tom's Guide</small></figcaption></figure></figure><p>To unlock the touchscreen’s full potential, you should download Turtle Beach’s Swarm II companion app on your Windows laptop. Via Swarm II, you can customize the touchscreen by adding widgets to it, or selecting a template preset, such as ‘KB7 CS.’ This is designed specifically for Counter-Strike, and it shows you handy information, such as your equipped weapon, bullets remaining, total kills, etc. You can add third-party apps to the touchscreen, too, like Discord, Steam, and StreamLabs. <em>This </em>is how you make a keyboard’s screen useful and not just a gimmick.</p><h2 id="top-tier-gaming-features">Top-tier gaming 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="T3haF4zGRKD5N3FxWHktS5" name="Turtlebeach_KB7_ 14.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/T3haF4zGRKD5N3FxWHktS5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Turtle Beach Command Series KB7 TKL is, first and foremost, a gaming keyboard. It’s fitted with in-house Titan low-profile Hall Effect switches, and it has an 8,000Hz polling rate. For 99% of gamers, 1,000Hz polling is more than enough. 8KHz is also a drain on system resources, but thankfully, you can drop the polling rate by launching Swarm II on your machine.</p><p>I tested the KB7 TKL by playing Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a>, and the keyboard didn’t let me down even once. I tried both 8KHz and 1KHz polling rates and didn’t notice any input lag or unresponsiveness. The Titan switches’ per-key adjustable actuation is the real star here, though. Their sensitivity can be adjusted between 0.1mm and 3.2mm.</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="Q7TNhdJQ89vY9NxesyvVV5" name="Turtlebeach_KB7_ 18.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/Q7TNhdJQ89vY9NxesyvVV5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 appreciated being able to adjust the sensitivity via the touchscreen itself, so I could quickly drop to a low actuation point of around 0.3mm for ultra-responsive gameplay in Counter-Strike 2. Turning the sensitivity of action and movement keys down meant that I could quickly swap between weapons by lightly tapping a key. Executing a series of complex actions felt like a walk in the park too, as I could sprint, throw a grenade, and crouch into hiding rapidly. This gave me a competitive advantage that often mechanical keyboards can’t offer.</p><p>In addition to that, you also get ReacTap, which is Turtle Beach’s term for Last Key Prioritization — a type of Simultaneous Opposite Key Detection (SOCD) resolution, or in other words, telling the keyboard what to do when two opposing commands are entered. </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:7215px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="fo2FYC5iYgYsDmbHJ7m4k6" name="TB_CSKB7TKL" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/fo2FYC5iYgYsDmbHJ7m4k6-1920-80.jpg" mos="" align="middle" fullscreen="" width="7215" height="4058" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Enabling this essentially prioritizes the last key pressed, and I use it primarily for counter-strafing. You can basically draw to a complete stop from a sideways strafe by pressing the opposing direction key (A and D or vice versa). This gets you kicked out of Valve’s games, like Counter-Strike 2 and DOTA, as it acts as a cheat code — but it’s not banned in Valorant and other non-Valve competitive games. It also works like a charm in single-player games, like Cyberpunk 2077 where I was running circles around NPCs.</p><p>Finally, there’s Rapid Trigger which ensures that all inputs are recognized by enabling near-instantaneous switch reset when lifting. This allows for quicker directional changes and micro-adjustments when dodging or repositioning, for example. In Cyberpunk 2077, I used it to quickly peek around corners. All of these elite features come together to deliver an exceptional gaming experience.</p><h2 id="decent-for-typing-too">Decent for typing, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6245px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hXKK3zJrMq4paVGgQ3V6e6" name="TB_CSKB7TKL-2" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/hXKK3zJrMq4paVGgQ3V6e6-1920-80.jpg" mos="" align="middle" fullscreen="" width="6245" height="3513" attribution="" endorsement="" class="inline"></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 I prefer one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a>, like my trusty <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139), for typing, the Turtle Beach Command Series KB7 TKL is a good deck for productivity, too. The first page of its touchscreen features functions like Print, Insert, Page Up and Page Down, which are more productivity-oriented. There are Copy and Paste shortcuts, which negate the need to hold down Cmd/Ctrl + C/V.</p><p>The KB7 TKL feels good to type on, too. To put it through its paces, I took a series of typing tests at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, with the keyboard set to different sensitives for each test.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Turtle Beach KB7 TKL (3.2mm actuation)</p></td><td  ><p>84</p></td><td  ><p>86%</p></td></tr><tr><td class="firstcol " ><p>Turtle Beach KB7 TKL (1.5mm)</p></td><td  ><p>89</p></td><td  ><p>90.1%</p></td></tr><tr><td class="firstcol " ><p>Turtle Beach KB7 TKL (0.5mm)</p></td><td  ><p>92</p></td><td  ><p>92%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/gravastar-mercury-v75-pro-he-review">Gravastar Mercury V75 Pro HE</a> (0.5mm)</p></td><td  ><p>101</p></td><td  ><p>89.01%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review">Keychron Q6 HE 8K</a> (0.5mm actuation)</p></td><td  ><p>106</p></td><td  ><p>89.02%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-q12-he-review">Keychron Q12 HE</a> (0.5mm actuation)</p></td><td  ><p>103</p></td><td  ><p>87.14%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> (0.5mm actuation)</p></td><td  ><p>102</p></td><td  ><p>93.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/nzxt-function-elite-mini-tkl-review">NZXT Function Elite MiniTKL</a> (0.6mm actuation)</p></td><td  ><p>89</p></td><td  ><p>88.42%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> (0.5mm actuation)</p></td><td  ><p>101</p></td><td  ><p>89.41%</p></td></tr></tbody></table></div><p>As you can see in the table above, the KB7 TKL performs well, but it doesn’t quite match up to other magnetic keyboards I’ve tested. That’s on me, though, as at the time of writing, I’d tested a bunch of mechanical keyboards leading up to the KB7 TKL, and was more accustomed to their tactile feel. I’ve personally enjoyed typing on the KB7 TKL. Its keycaps feel soft and polished to touch, and with the actuation set just right, my fingers felt like they were gliding across the board.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-the-downs"><span>Turtle Beach Command Series KB7 TKL review: The downs</span></h2><p>Annoyingly, the Turtle Beach Command Series KB7 TKL isn’t hot-swappable and can’t be used wirelessly, and it’s expensive, too.</p><h2 id="not-hot-swappable-2">Not hot-swappable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ojVrYWrqGMe95pkQPCvNU5" name="Turtlebeach_KB7_ 17.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/ojVrYWrqGMe95pkQPCvNU5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest drawbacks of the Turtle Beach Command Series KB7 TKL is that it isn’t hot-swappable. That means its Titan Hall Effect switches are soldered to the PCB, so you can’t just pop them out and replace them with switches of your choice. Having tested nearly 100 keyboards at the time of writing, that’s a fairly significant drawback, especially when Turtle Beach’s competitors have been doing it for a while now.</p><p>If you want a hot-swappable keyboard, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review">Keychron K2 HE</a> ($130) or the <a href="https://www.tomsguide.com/computing/peripherals/wooting-80he-review">Wooting 80HE</a> ($199). At the time of writing, Hall Effect switch choice is fairly limited, but we expect more switches to pop up in the market as these keyboards gain more popularity and become more mainstream.</p><h2 id="wired-only-2">Wired-only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="vfDkUdE82peZCzVJK3B3M5" name="Turtlebeach_KB7_ 15.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/vfDkUdE82peZCzVJK3B3M5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 more of a pre-purchase consideration rather than an outright flaw: the Turtle Beach Command Series KB7 TKL is a wired-only keyboard. You can connect the keyboard to your machine either via USB-C or two USB-A ports (using the included adapter), which is similar to the <a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Turtle Beach Vulcan II Max</a> ($229). You also get USB-A passthrough for connecting other devices straight to the KB7 TKL.</p><p>If you want to use a Hall Effect keyboard on the go and would like wireless connectivity, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> ($99) or the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag </a>($99).</p><h2 id="expensive">Expensive</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="68kynPamz7GBQNLuTx7cS5" name="Turtlebeach_KB7_ 20.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/68kynPamz7GBQNLuTx7cS5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Turtle Beach makes some great peripherals but they don’t always come cheap — and the same goes for the KB7 TKL. The keyboard retails for <a href="https://www.amazon.com/Turtle-Beach-Command-Hall-Effect-Keyboard/dp/B0GWD2D7S3/" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Turtle-Beach-Command-Gaming-Keyboard/dp/B0GXZN2JNK/" target="_blank" rel="nofollow">£159 at Amazon</a> so while it isn’t as expensive as the Keychron Q12 HE ($249) or the <a href="https://www.tomsguide.com/computing/peripherals/lemokey-l5-he-review">Lemokey L5 HE</a> ($220), it isn’t the cheapest either. That price tag hurts more when you take into consideration that the KB7 TKL isn’t hot-swappable and doesn’t offer wireless connectivity.</p><p>The KB7 TKL faces fierce competition from other Hall Effect keyboard manufacturers — and you don’t need to spend nearly $200 or more to get a decent one nowadays. The Keychron K2 HE ($130), the Epomaker HE65 Mag ($99) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air60-he-tiny-profile-huge-performance">NuPhy Air60 HE </a>($139) are exceptional magnetic keyboards that don’t break the bank.</p><h2 class="article-body__section" id="section-turtle-beach-command-series-kb7-tkl-review-verdict"><span>Turtle Beach Command Series KB7 TKL 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="JGs9D56WpJFASoXQQ7p5G5" name="Turtlebeach_KB7_ 3.JPG" alt="A Turtle Beach Command Series KB7 TKL Hall Effect keyboard" src="https://cdn.mos.cms.futurecdn.net/JGs9D56WpJFASoXQQ7p5G5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 powerful gaming keyboard, the Turtle Beach Command Series KB7 TKL is a standout choice as the keyboard doesn’t give you too much to complain about. It packs 8,000Hz polling, adjustable actuation, and top-tier gaming features like Rapid Trigger and ReacTap. In addition to that, you get a large customizable touchscreen that enables you to adjust settings on the go.</p><p>But unfortunately, the KB7 TKL has its flaws. It isn’t hot-swappable, which most of its competitors are, and it isn’t wireless either. It’s also expensive and is simply outdone by the likes of Epomaker and Keychron offering better value for money.</p><p>If you aren’t bothered by those drawbacks, then the KB7 TKL could be perfect for you. I, for one, absolutely love how helpful the touchscreen is, and now I wish all keyboards had it.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Logitech Mobi Fold review: This foldable mouse has been an interesting addition to my workflow, and I’ve been surprised by how useful it is ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Logitech Mobi Fold seems pretty unconventional at first glance. It’s a mouse that folds into a net square so it fits in the palm of your hand, making it easy to take with you anywhere. But is it one of the <a href="https://www.tomsguide.com/best-picks/best-mouse">best mice</a>? Well, it’s certainly one of the most interesting. </p><p>The design is surprisingly comfortable, matching the contours of my palm perfectly, and the smart sensors help me avoid any misclicks when moving around. The mouse is also compatible with the Logi Options+ App, so you can make changes to DPI based on your needs. </p><p>But the short battery life, jumpy touch-sensitive scrolling and loud clicks might put you off. To find out more about the foldable mouse and its quirks, you can keep reading this full Logitech Mobi Fold review. </p><h2 class="article-body__section" id="section-logitech-mobi-fold-review-cheat-sheet"><span>Logitech Mobi Fold review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A foldable mouse</li><li><strong>Who is it for? </strong>On-the-go workers</li><li><strong>How much does it cost?</strong> The Logitech Mobi Fold is available for <a href="https://www.amazon.com/Logitech-Foldable-Wireless-Mouse-Graphite/dp/B0GSXLK8SC" target="_blank" rel="nofollow">$79</a> / <a href="https://www.amazon.co.uk/Logitech-Mobi-Ultra-Portable-Travel-Mouse-Graphite/dp/B0GX6V8CJF" target="_blank" rel="nofollow">£69</a> on Amazon</li><li><strong>What do we like?</strong> It’s easy to transport, comfortable and customizable</li><li><strong>What don’t we like?</strong> The short battery life is disappointing, scrolling is jumpy and the clicks are loud</li></ul><h2 class="article-body__section" id="section-logitech-mobi-fold-review-specs"><span>Logitech Mobi Fold 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/Logitech-Foldable-Wireless-Mouse-Graphite/dp/B0GSXLK8SC" target="_blank" rel="nofollow">$79</a> / <a href="https://www.amazon.co.uk/Logitech-Mobi-Ultra-Portable-Travel-Mouse-Graphite/dp/B0GX6V8CJF" target="_blank" rel="nofollow">£69</a></p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>4.8 x 2.3 x 1.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (when folded)</strong></p></td><td  ><p>2.6 x 0.8 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.8 oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Lilac, Graphite, Off-white</p></td></tr><tr><td class="firstcol " ><p><strong>DPI</strong></p></td><td  ><p>400 - 4000</p></td></tr><tr><td class="firstcol " ><p><strong>Number of buttons</strong></p></td><td  ><p>4</p></td></tr><tr><td class="firstcol " ><p><strong>Material</strong></p></td><td  ><p>Plastic, Silicone</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wireless</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>32 days</p></td></tr><tr><td class="firstcol " ><p><strong>Compatibility</strong></p></td><td  ><p>Windows 10 or later, macOS 12 or later, iPadOS 15 or later, ChromeOS, Android 12 or later, Linux</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-logitech-mobi-fold-review-the-ups"><span>Logitech Mobi Fold review: The ups</span></h2><p>The Logitech Mobi Fold is one to consider if you regularly work on the go. The foldable design is comfortable to use and convenient, while the customizable buttons and sensors mean you can adjust it to suit your needs. </p><h2 id="it-does-yoga">It does yoga</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yvXCck549emzLaSRVk6G9U" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/yvXCck549emzLaSRVk6G9U-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold does exactly what the name suggests and can be contorted much like a flip phone. The mouse can be quickly snapped together, reducing it from 4.8 x 2.3 x 1.3 inches to just 2.6 x 0.8 inches. </p><p>Honestly, the mouse is compact anyway, but this neat folding means it can slot easily into the side pocket of my laptop cover so I know exactly where it is. I’ve found it to be an ideal option for when I’m tight on space in my bag but still need a reliable mouse on hand. </p><p>I’ve also tested the <a href="https://www.tomsguide.com/computing/peripherals/esr-magmouse-review">ESR MagMouse</a> ($35), which is designed for on-the-go use. The difference here is that the ESR is still the usual mouse shape. Its only adaptation is that its base is magnetic, so it can be attached to your laptop when moving around. I think the Logitech Mobi Fold is a lot more useful than this because it becomes so compact and can be stored in a small pocket when moving around, while the ESR is a typically clunky mouse and measures 4.2 x 2.5 x 1.5 inches.  </p><h2 id="comfortable-to-hold">Comfortable to hold</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5ZBVwyw3f5JnYt7p5u2bfU" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/5ZBVwyw3f5JnYt7p5u2bfU-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Because of the way the Logitech Mobi Fold is angled, I’ve found it incredibly comfortable to hold. I usually like to use a vertical mouse, like the <a href="https://www.tomsguide.com/computing/peripherals/anker-2-4g-wireless-vertical-ergonomic-mouse-review">Anker 2.4G Wireless Vertical Ergonomic</a> ($25), as the shape supports my hand in a comfortable position all day long, but this style isn’t portable since it’s very large and awkwardly shaped. </p><p>The Logitech Mobi Fold bridges the gap between comfort and a portable design. The fold has enough of an arch that my palm feels cradled and supported. However, this is personal, and anyone with large hands will probably think the size of this mouse is frankly ridiculous. </p><h2 id="no-misclicks">No misclicks</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="oix3H6fKsZeJqRdBxNQj3U" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/oix3H6fKsZeJqRdBxNQj3U-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 picking up the Logitech Mobi Fold to collapse and store it, I was concerned I would accidentally hit a few buttons and do something detrimental to my spreadsheet. But I didn’t need to worry. </p><p>The mouse uses sensors to detect when it is lifted or folded, so the buttons become deactivated, meaning zero misclicks.</p><h2 id="loads-of-options">Loads of Options</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="qq6d2RoLCNLSn8tKPhN2ST" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/qq6d2RoLCNLSn8tKPhN2ST-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold is compatible with the Logi Options+ App. I’m familiar with this app from previously testing Logitech’s keyboards and webcams. It’s just as easy to use with the mouse. The mouse has four buttons, and two of these are customizable in the app. Having this app means you can adjust the mouse DPI, making it more or less sensitive depending on your needs. I left the right and left buttons as they were, but decreased the DPI so the mouse was less sensitive to my movements. This gave me a lot more control.</p><h2 class="article-body__section" id="section-logitech-mobi-fold-review-the-downs"><span>Logitech Mobi Fold review: The downs</span></h2><p>As much as I like the Logitech Mobi Fold, it has a few quirks. The scrolling is jumpy, and the clicks are loud. There’s also no USB receiver included, and the battery life is very disappointing. </p><h2 id="scrolling-is-jumpy">Scrolling is jumpy</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ax6f6ZKVQr2SvBaz9DvgGU" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ax6f6ZKVQr2SvBaz9DvgGU-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Scrolling on the Logitech Mobi Fold feels jumpy. The scroll is touch-sensitive, and the absence of a scroll wheel makes the mouse look much more streamlined. But at times, this is too sensitive and sometimes even unresponsive. </p><p>This makes it pretty unreliable, and I found myself moving to the scroll pad on my laptop when I needed to scroll. I’d find this frustrating if I were using the mouse every day. </p><h2 id="clicks-are-loud">Clicks are 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="ZQGkiM8kwUMasQC6DQfwaU" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ZQGkiM8kwUMasQC6DQfwaU-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold looks discreet, the sound it makes is anything but. The clicks are quite loud, and in an office setting, this would become annoying very fast. Not only is it distracting for coworkers, but I found it distracted me from my own workflow. I would have appreciated some quieter switches. </p><h2 id="usb-receiver-not-included">USB receiver not included</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LsbFqiKcSDzpjqVWdMmgsT" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/LsbFqiKcSDzpjqVWdMmgsT-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold can be connected to your device via Bluetooth or by using a USB receiver. However, the USB receiver isn’t included with the mouse. Instead, you have to purchase the <a href="https://www.amazon.com/Logitech-Bolt-USB-C-Receiver/dp/B0F9DWFSHP">Logitech Bolt USB-C Receiver separately for $14</a>. </p><p>Since this USB receiver is universal, it can be used with other Logitech devices, but it’s a shame it’s not included with the mouse. It’s unusual to see a mouse in this price range without a receiver; for example, the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini V2 8K ($69)</a>, <a href="https://www.tomsguide.com/computing/peripherals/nzxt-lift-elite-wireless-gaming-mouse-review">NZXT Lift Elite Wireless</a> ($74) and <a href="https://www.tomsguide.com/computing/peripherals/cherry-xtrfy-m50-wireless-gaming-mouse-review">Cherry XTRFY M50</a> ($69) all include them. </p><p>The Logitech Pebble Mouse 2 could also be a viable option if you want a USB receiver and need to save some space, as the design is very slim. It’s also only <a href="https://www.amazon.com/Logitech-Bluetooth-Lightweight-Customizable-Easy-Switch/dp/B0BT48897L">$24 on Amazon U.S.</a></p><h2 id="poor-battery-life">Poor battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eAq3vSzAPWAtpmdXusAgjT" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/eAq3vSzAPWAtpmdXusAgjT-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold has very disappointing battery life. These numbers depend on your usage, but Logitech says you should get 32 days using Bluetooth and 33 days using the Bolt USB receiver. </p><p>In comparison, the ESR MagMouse has 80 days of battery life, and if that’s not enough, the <a href="https://www.tomsguide.com/computing/peripherals/logitech-signature-ai-edition-m750-review">Logitech Signature AI Edition</a> ($49) has a whopping 24 months. </p><p>The Mobi Fold is rechargeable via USB-C and only takes around 45 minutes to fully charge. However, it’s worth noting that the mouse is hard to use when it’s charging due to the position of the charging port, so you’ll spend those 45 minutes waiting around. </p><h2 class="article-body__section" id="section-logitech-mobi-fold-review-verdict"><span>Logitech Mobi Fold review: Verdict</span></h2><p>The Logitech Mobi Fold is one of the most interesting mice I’ve come across. Its foldable style makes it highly portable while still being comfortable to hold as it cradles your palm. The smart sensors helped me to avoid any misclicks when transporting the mouse, and the ability to use the Logi Options+ app means you can make any changes you need to the mouse to suit your work style.</p><p>But unfortunately, there are some letdowns. The short 32-day battery life is very low compared to others in this price range. The touch-sensitive scrolling is also jumpy, and the clicks are loud, making the mouse a little distracting. </p><p>If you’re someone who regularly works on the go, I think the Logitech Mobi Fold is an ideal solution to save space and remain productive. But for everyday use at your desk, I think it leaves a lot to be desired. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/mice/logitech-mobi-fold-review</link>
                                                                            <description>
                            <![CDATA[ The Logitech Mobi Fold is a useful addition to your workflow if you’re constantly on the go, but watch out for that battery life. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">kRkn6MbjqCHFMiW5aRCc8J</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/FG2TnWGGfmpxYFib2BmWLT-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 27 Aug 2026 13:13:26 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Mice]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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-320-70.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/FG2TnWGGfmpxYFib2BmWLT-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&amp;#39;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Logitech Mobi Fold mouse in graphite photographed in front of a blue background]]></media:description>                                                            <media:text><![CDATA[Logitech Mobi Fold mouse in graphite photographed in front of a blue background]]></media:text>
                                <media:title type="plain"><![CDATA[Logitech Mobi Fold mouse in graphite photographed in front of a blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/FG2TnWGGfmpxYFib2BmWLT-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Logitech Mobi Fold seems pretty unconventional at first glance. It’s a mouse that folds into a net square so it fits in the palm of your hand, making it easy to take with you anywhere. But is it one of the <a href="https://www.tomsguide.com/best-picks/best-mouse">best mice</a>? Well, it’s certainly one of the most interesting. </p><p>The design is surprisingly comfortable, matching the contours of my palm perfectly, and the smart sensors help me avoid any misclicks when moving around. The mouse is also compatible with the Logi Options+ App, so you can make changes to DPI based on your needs. </p><p>But the short battery life, jumpy touch-sensitive scrolling and loud clicks might put you off. To find out more about the foldable mouse and its quirks, you can keep reading this full Logitech Mobi Fold review. </p><h2 class="article-body__section" id="section-logitech-mobi-fold-review-cheat-sheet"><span>Logitech Mobi Fold review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A foldable mouse</li><li><strong>Who is it for? </strong>On-the-go workers</li><li><strong>How much does it cost?</strong> The Logitech Mobi Fold is available for <a href="https://www.amazon.com/Logitech-Foldable-Wireless-Mouse-Graphite/dp/B0GSXLK8SC" target="_blank" rel="nofollow">$79</a> / <a href="https://www.amazon.co.uk/Logitech-Mobi-Ultra-Portable-Travel-Mouse-Graphite/dp/B0GX6V8CJF" target="_blank" rel="nofollow">£69</a> on Amazon</li><li><strong>What do we like?</strong> It’s easy to transport, comfortable and customizable</li><li><strong>What don’t we like?</strong> The short battery life is disappointing, scrolling is jumpy and the clicks are loud</li></ul><h2 class="article-body__section" id="section-logitech-mobi-fold-review-specs"><span>Logitech Mobi Fold 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/Logitech-Foldable-Wireless-Mouse-Graphite/dp/B0GSXLK8SC" target="_blank" rel="nofollow">$79</a> / <a href="https://www.amazon.co.uk/Logitech-Mobi-Ultra-Portable-Travel-Mouse-Graphite/dp/B0GX6V8CJF" target="_blank" rel="nofollow">£69</a></p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>4.8 x 2.3 x 1.3 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions (when folded)</strong></p></td><td  ><p>2.6 x 0.8 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.8 oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Lilac, Graphite, Off-white</p></td></tr><tr><td class="firstcol " ><p><strong>DPI</strong></p></td><td  ><p>400 - 4000</p></td></tr><tr><td class="firstcol " ><p><strong>Number of buttons</strong></p></td><td  ><p>4</p></td></tr><tr><td class="firstcol " ><p><strong>Material</strong></p></td><td  ><p>Plastic, Silicone</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wireless</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life</strong></p></td><td  ><p>32 days</p></td></tr><tr><td class="firstcol " ><p><strong>Compatibility</strong></p></td><td  ><p>Windows 10 or later, macOS 12 or later, iPadOS 15 or later, ChromeOS, Android 12 or later, Linux</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-logitech-mobi-fold-review-the-ups"><span>Logitech Mobi Fold review: The ups</span></h2><p>The Logitech Mobi Fold is one to consider if you regularly work on the go. The foldable design is comfortable to use and convenient, while the customizable buttons and sensors mean you can adjust it to suit your needs. </p><h2 id="it-does-yoga">It does yoga</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="yvXCck549emzLaSRVk6G9U" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/yvXCck549emzLaSRVk6G9U-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold does exactly what the name suggests and can be contorted much like a flip phone. The mouse can be quickly snapped together, reducing it from 4.8 x 2.3 x 1.3 inches to just 2.6 x 0.8 inches. </p><p>Honestly, the mouse is compact anyway, but this neat folding means it can slot easily into the side pocket of my laptop cover so I know exactly where it is. I’ve found it to be an ideal option for when I’m tight on space in my bag but still need a reliable mouse on hand. </p><p>I’ve also tested the <a href="https://www.tomsguide.com/computing/peripherals/esr-magmouse-review">ESR MagMouse</a> ($35), which is designed for on-the-go use. The difference here is that the ESR is still the usual mouse shape. Its only adaptation is that its base is magnetic, so it can be attached to your laptop when moving around. I think the Logitech Mobi Fold is a lot more useful than this because it becomes so compact and can be stored in a small pocket when moving around, while the ESR is a typically clunky mouse and measures 4.2 x 2.5 x 1.5 inches.  </p><h2 id="comfortable-to-hold">Comfortable to hold</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5ZBVwyw3f5JnYt7p5u2bfU" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/5ZBVwyw3f5JnYt7p5u2bfU-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Because of the way the Logitech Mobi Fold is angled, I’ve found it incredibly comfortable to hold. I usually like to use a vertical mouse, like the <a href="https://www.tomsguide.com/computing/peripherals/anker-2-4g-wireless-vertical-ergonomic-mouse-review">Anker 2.4G Wireless Vertical Ergonomic</a> ($25), as the shape supports my hand in a comfortable position all day long, but this style isn’t portable since it’s very large and awkwardly shaped. </p><p>The Logitech Mobi Fold bridges the gap between comfort and a portable design. The fold has enough of an arch that my palm feels cradled and supported. However, this is personal, and anyone with large hands will probably think the size of this mouse is frankly ridiculous. </p><h2 id="no-misclicks">No misclicks</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="oix3H6fKsZeJqRdBxNQj3U" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/oix3H6fKsZeJqRdBxNQj3U-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 picking up the Logitech Mobi Fold to collapse and store it, I was concerned I would accidentally hit a few buttons and do something detrimental to my spreadsheet. But I didn’t need to worry. </p><p>The mouse uses sensors to detect when it is lifted or folded, so the buttons become deactivated, meaning zero misclicks.</p><h2 id="loads-of-options">Loads of Options</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="qq6d2RoLCNLSn8tKPhN2ST" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/qq6d2RoLCNLSn8tKPhN2ST-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold is compatible with the Logi Options+ App. I’m familiar with this app from previously testing Logitech’s keyboards and webcams. It’s just as easy to use with the mouse. The mouse has four buttons, and two of these are customizable in the app. Having this app means you can adjust the mouse DPI, making it more or less sensitive depending on your needs. I left the right and left buttons as they were, but decreased the DPI so the mouse was less sensitive to my movements. This gave me a lot more control.</p><h2 class="article-body__section" id="section-logitech-mobi-fold-review-the-downs"><span>Logitech Mobi Fold review: The downs</span></h2><p>As much as I like the Logitech Mobi Fold, it has a few quirks. The scrolling is jumpy, and the clicks are loud. There’s also no USB receiver included, and the battery life is very disappointing. </p><h2 id="scrolling-is-jumpy">Scrolling is jumpy</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ax6f6ZKVQr2SvBaz9DvgGU" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ax6f6ZKVQr2SvBaz9DvgGU-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Scrolling on the Logitech Mobi Fold feels jumpy. The scroll is touch-sensitive, and the absence of a scroll wheel makes the mouse look much more streamlined. But at times, this is too sensitive and sometimes even unresponsive. </p><p>This makes it pretty unreliable, and I found myself moving to the scroll pad on my laptop when I needed to scroll. I’d find this frustrating if I were using the mouse every day. </p><h2 id="clicks-are-loud">Clicks are 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="ZQGkiM8kwUMasQC6DQfwaU" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ZQGkiM8kwUMasQC6DQfwaU-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold looks discreet, the sound it makes is anything but. The clicks are quite loud, and in an office setting, this would become annoying very fast. Not only is it distracting for coworkers, but I found it distracted me from my own workflow. I would have appreciated some quieter switches. </p><h2 id="usb-receiver-not-included">USB receiver not included</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LsbFqiKcSDzpjqVWdMmgsT" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/LsbFqiKcSDzpjqVWdMmgsT-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold can be connected to your device via Bluetooth or by using a USB receiver. However, the USB receiver isn’t included with the mouse. Instead, you have to purchase the <a href="https://www.amazon.com/Logitech-Bolt-USB-C-Receiver/dp/B0F9DWFSHP">Logitech Bolt USB-C Receiver separately for $14</a>. </p><p>Since this USB receiver is universal, it can be used with other Logitech devices, but it’s a shame it’s not included with the mouse. It’s unusual to see a mouse in this price range without a receiver; for example, the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini V2 8K ($69)</a>, <a href="https://www.tomsguide.com/computing/peripherals/nzxt-lift-elite-wireless-gaming-mouse-review">NZXT Lift Elite Wireless</a> ($74) and <a href="https://www.tomsguide.com/computing/peripherals/cherry-xtrfy-m50-wireless-gaming-mouse-review">Cherry XTRFY M50</a> ($69) all include them. </p><p>The Logitech Pebble Mouse 2 could also be a viable option if you want a USB receiver and need to save some space, as the design is very slim. It’s also only <a href="https://www.amazon.com/Logitech-Bluetooth-Lightweight-Customizable-Easy-Switch/dp/B0BT48897L">$24 on Amazon U.S.</a></p><h2 id="poor-battery-life">Poor battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eAq3vSzAPWAtpmdXusAgjT" name="Logitech Mobi Fold" alt="Logitech Mobi Fold mouse in graphite photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/eAq3vSzAPWAtpmdXusAgjT-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Mobi Fold has very disappointing battery life. These numbers depend on your usage, but Logitech says you should get 32 days using Bluetooth and 33 days using the Bolt USB receiver. </p><p>In comparison, the ESR MagMouse has 80 days of battery life, and if that’s not enough, the <a href="https://www.tomsguide.com/computing/peripherals/logitech-signature-ai-edition-m750-review">Logitech Signature AI Edition</a> ($49) has a whopping 24 months. </p><p>The Mobi Fold is rechargeable via USB-C and only takes around 45 minutes to fully charge. However, it’s worth noting that the mouse is hard to use when it’s charging due to the position of the charging port, so you’ll spend those 45 minutes waiting around. </p><h2 class="article-body__section" id="section-logitech-mobi-fold-review-verdict"><span>Logitech Mobi Fold review: Verdict</span></h2><p>The Logitech Mobi Fold is one of the most interesting mice I’ve come across. Its foldable style makes it highly portable while still being comfortable to hold as it cradles your palm. The smart sensors helped me to avoid any misclicks when transporting the mouse, and the ability to use the Logi Options+ app means you can make any changes you need to the mouse to suit your work style.</p><p>But unfortunately, there are some letdowns. The short 32-day battery life is very low compared to others in this price range. The touch-sensitive scrolling is also jumpy, and the clicks are loud, making the mouse a little distracting. </p><p>If you’re someone who regularly works on the go, I think the Logitech Mobi Fold is an ideal solution to save space and remain productive. But for everyday use at your desk, I think it leaves a lot to be desired. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I’m a laptop reviewer — 12 essential back-to-school laptop accessories under $50 I recommend ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The <a href="https://www.tomsguide.com/computing/laptops/best-laptops">best laptops</a> are a student’s most important ally, especially now during the back-to-school season. Though you can certainly make do with a laptop out of the box, you can make your notebook even more productive with the right accessories. Best of all, you don’t have to break the bank to get everything you’d need.</p><p>I haven’t been inside a classroom in decades, but I review laptops for a living and know the best items to recommend. This includes things like laptop stands, USB hubs, privacy screens, wireless peripherals, and more. Best of all, you can nab everything I’ve listed below for under $50 each, as being a student can already be pretty expensive.</p><p>To that end, here are the 12 best laptop accessories I recommend right now. I've also included the two <a href="https://www.tomsguide.com/best-picks/best-budget-laptops">best budget laptops</a> to get right now. Also, check out our <a href="https://www.tomsguide.com/news/ultimate-back-to-school-guide">Ultimate Back-to-School Guide </a>2026 for more deals. Okay, let's dive in!</p><h3 class="article-body__section" id="section-quick-links"><span>Quick Links</span></h3><ul><li><strong>Mosiso Laptop Sleeve: </strong><a href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow"><strong>was $14 now $13 @ Amazon</strong></a><strong></strong></li><li><strong>Matein Travel Laptop Backpack: </strong><a href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow"><strong>was $33 now $18 @ Amazon</strong></a><strong></strong></li><li><strong>Havit Laptop Cooling Pad: </strong><a href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow"><strong>was $34 now $25 @ Amazon</strong></a></li><li><strong>Lamicall Laptop Stand Riser: </strong><a href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow"><strong>was $35 now $29 @ Amazon</strong></a><strong></strong></li><li><strong>Logitech Signature M650: </strong><a href="https://www.amazon.com/dp/B09NSCZVB1" target="_blank" rel="nofollow"><strong>was $39 now $30 @ Amazon</strong></a><strong></strong></li><li><strong>SightPro Magnetic Laptop Privacy Screen: </strong><a href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" target="_blank" rel="nofollow"><strong>$34 @ Amazon</strong></a><strong></strong></li><li><strong>Acer USB-C Docking Station: </strong><a href="https://www.amazon.com/Docking-Station-Monitor-Charging-Compatible/dp/B0DW85W3FV" target="_blank" rel="nofollow"><strong>was $47 now $36 @ Amazon</strong></a></li><li><strong>Keychron B35 Wireless Keyboard: </strong><a href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" target="_blank" rel="nofollow"><strong>$39 @ Amazon</strong></a><strong></strong></li><li><strong>LapGear Lap Desk: </strong><a href="https://www.amazon.com/LapGear-Office-wrist-mouse-holder/dp/B07N9L5934" target="_blank" rel="nofollow"><strong>was $49 now $39 @ Amazon</strong></a></li><li><strong>UGreen 100W USB Multi-Power Charger: </strong><a href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" target="_blank" rel="nofollow"><strong>was $54 now $42 @ Amazon</strong></a><strong></strong></li><li><strong>Iniu Laptop Power Bank: </strong><a href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow"><strong>was $50 now $42 @ Amazon</strong></a><strong></strong></li><li><strong>MNN Portable Monitor: </strong><a href="https://www.amazon.com/MNN-Portable-15-6inch-Ultra-Slim-Speakers/dp/B0B9NNWXVP" target="_blank" rel="nofollow"><strong>was $49 now $44 @ Amazon</strong></a></li></ul><h3 class="article-body__section" id="section-the-two-best-budget-laptops"><span>The two best budget laptops</span></h3><p>If you're looking for an excellent budget laptop, the <a href="https://www.tomsguide.com/computing/macbooks/apple-macbook-neo-review">MacBook Neo</a> and new <a href="https://www.tomsguide.com/computing/laptops/dell-xps-13-2026-review">Dell XPS 13 (2026)</a> are both available starting at $699. So whether you're a macOS or Windows user, you won't be stuck with a subpar machine for school!</p><div class="product"><a data-dimension112="be0a445e-97f5-11f1-b67e-f9a7962276af" data-action="Deal Block" data-label="XPS 13 (2026)" data-dimension48="XPS 13 (2026)" data-dimension25="$699" href="https://www.dell.com/en-us/shop/dell-laptops/new-xps-13-laptop/spd/xps13dx13260laptop" 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:100.00%;"><img id="yVpTsWNESwCtxxLQmbPTgh" name="Dell XPS 13 (2026) deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/yVpTsWNESwCtxxLQmbPTgh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.dell.com/en-us/shop/dell-laptops/new-xps-13-laptop/spd/xps13dx13260laptop" target="_blank" rel="nofollow" data-dimension112="be0a445e-97f5-11f1-b67e-f9a7962276af" data-action="Deal Block" data-label="XPS 13 (2026)" data-dimension48="XPS 13 (2026)" data-dimension25="$699">XPS 13 (2026): $699</a></strong><br>The new Dell XPS 13 is a fantastic back-to-school laptop. This model has a 13.4-inch InfinityEdge 2.5K touch display, an Intel Core Series 3 "Wildcat Lake" CPU, 8GB of RAM, and a 512GB SSD. It's wrapped in an aluminum design that's just 0.5 inches thick and weighs 2.2 pounds.<a class="view-deal button" href="https://www.dell.com/en-us/shop/dell-laptops/new-xps-13-laptop/spd/xps13dx13260laptop" target="_blank" rel="nofollow" data-dimension112="be0a445e-97f5-11f1-b67e-f9a7962276af" data-action="Deal Block" data-label="XPS 13 (2026)" data-dimension48="XPS 13 (2026)" data-dimension25="$699">View Deal</a></p></div><div class="product"><a data-dimension112="be0a44d6-97f5-11f1-bbbf-112e2a81b715" data-action="Deal Block" data-label="13" MacBook Neo" data-dimension48="13" MacBook Neo" data-dimension25="$689" href="https://www.amazon.com/gp/aw/d/B0GR6FHGXX" 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:61.07%;"><img id="Lg4Dvg68j9SbB5CPNrTEpH" name="MacBook Neo deal block" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="916" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/gp/aw/d/B0GR6FHGXX" target="_blank" rel="nofollow" data-dimension112="be0a44d6-97f5-11f1-bbbf-112e2a81b715" data-action="Deal Block" data-label='13" MacBook Neo' data-dimension48='13" MacBook Neo' data-dimension25="$689">13" MacBook Neo: was $699 now $689</a></strong><br>The MacBook Neo is Apple's most affordable Mac. The $699 laptop sports a 13-inch Liquid Retina (2408 x 1506) display, an A18 Pro chipset, 8GB of RAM, a 256GB SSD, and a 1080p FaceTime HD camera. There's also the option to add Touch ID and a 512GB SSD for a total price of $799. The 2.7-lb. laptop is available in Indigo, Blush, Citrus, and Silver.<a class="view-deal button" href="https://www.amazon.com/gp/aw/d/B0GR6FHGXX" target="_blank" rel="nofollow" data-dimension112="be0a44d6-97f5-11f1-bbbf-112e2a81b715" data-action="Deal Block" data-label="13" MacBook Neo" data-dimension48="13" MacBook Neo" data-dimension25="$689">View Deal</a></p></div><h3 class="article-body__section" id="section-12-back-to-school-laptop-accessories"><span>12 back-to-school laptop accessories</span></h3><div class="product"><a data-dimension112="4a88e4fc-97f7-11f1-9f6f-ff71000c42b7" data-action="Deal Block" data-label="Adjustable Laptop Stand" data-dimension48="Adjustable Laptop Stand" data-dimension25="$29" href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:979px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="uBkMygLLvTZMUbpkEm4kbG" name="lamicallgrid1.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/uBkMygLLvTZMUbpkEm4kbG-1920-80.jpg" mos="" align="middle" fullscreen="" width="979" height="979" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow" data-dimension112="4a88e4fc-97f7-11f1-9f6f-ff71000c42b7" data-action="Deal Block" data-label="Adjustable Laptop Stand" data-dimension48="Adjustable Laptop Stand" data-dimension25="$29">Adjustable Laptop Stand: was $35 now $29</a></strong><br>Lamicall's laptop stand is the best overall because it offers plenty of height and angle adjustments, a built-in heat vent design, and large rubber grips with plenty of friction to keep your laptop steady while typing. Plus, it’s foldable.<a class="view-deal button" href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow" data-dimension112="4a88e4fc-97f7-11f1-9f6f-ff71000c42b7" data-action="Deal Block" data-label="Adjustable Laptop Stand" data-dimension48="Adjustable Laptop Stand" data-dimension25="$29">View Deal</a></p></div><div class="product"><a data-dimension112="4a88e57e-97f7-11f1-880f-17895fcddf05" data-action="Deal Block" data-label="Nexode 100W GaN USB C Charger" data-dimension48="Nexode 100W GaN USB C Charger" data-dimension25="$42" href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" 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:100.00%;"><img id="JAPBAVnHQsSe34eMHtrMN6" name="Ugreen Nexode 100W charger.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/JAPBAVnHQsSe34eMHtrMN6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" target="_blank" rel="nofollow" data-dimension112="4a88e57e-97f7-11f1-880f-17895fcddf05" data-action="Deal Block" data-label="Nexode 100W GaN USB C Charger" data-dimension48="Nexode 100W GaN USB C Charger" data-dimension25="$42">Nexode 100W GaN USB C Charger: was $54 now $42</a></strong><br>The Ugreen Nexode 100W has three USB ports for charging your electronic devices, including tablets, smartphones, and even laptops. This is a great product for anyone with multiple electronic devices.<a class="view-deal button" href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" target="_blank" rel="nofollow" data-dimension112="4a88e57e-97f7-11f1-880f-17895fcddf05" data-action="Deal Block" data-label="Nexode 100W GaN USB C Charger" data-dimension48="Nexode 100W GaN USB C Charger" data-dimension25="$42">View Deal</a></p></div><div class="product"><a data-dimension112="4b2dc48a-97f8-11f1-b5f2-d72e9c789050" data-action="Deal Block" data-label="Signature M650" data-dimension48="Signature M650" data-dimension25="$30" href="https://www.amazon.com/dp/B09NSCZVB1" 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:100.00%;"><img id="5eKWVc6aPV8Y3XZhwwnft4" name="Logitech Signature M650" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/5eKWVc6aPV8Y3XZhwwnft4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Perfect for casual clickers or those who don’t need to fork out for specialized ergonomic mice, the Logitech Signature M650 is a great all-rounder with two connection options and customizable shortcuts. However, it may not be comfortable enough for long term professional use.<a class="view-deal button" href="https://www.amazon.com/dp/B09NSCZVB1" target="_blank" rel="nofollow" data-dimension112="4b2dc48a-97f8-11f1-b5f2-d72e9c789050" data-action="Deal Block" data-label="Signature M650" data-dimension48="Signature M650" data-dimension25="$30">View Deal</a></p></div><div class="product"><a data-dimension112="da30bf74-97f9-11f1-a978-95a599440651" data-action="Deal Block" data-label="Laptop Power Bank" data-dimension48="Laptop Power Bank" data-dimension25="$42" href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1209px;"><p class="vanilla-image-block" style="padding-top:124.07%;"><img id="pqupwK4PpvFWLZssdRV3zV" name="INIU P62" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/pqupwK4PpvFWLZssdRV3zV-1920-80.jpg" mos="" align="middle" fullscreen="" width="1209" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow" data-dimension112="da30bf74-97f9-11f1-a978-95a599440651" data-action="Deal Block" data-label="Laptop Power Bank" data-dimension48="Laptop Power Bank" data-dimension25="$42">Laptop Power Bank: was $50 now $42</a></strong><br>This power bank delivers up to 65W high-speed charging for two devices at once. The 20,000mAh power bank packs INIU's high-density TinyCell and groundbreaking HyperStack technology, which is perfect if you don't like traveling with heavy bricks.<a class="view-deal button" href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow" data-dimension112="da30bf74-97f9-11f1-a978-95a599440651" data-action="Deal Block" data-label="Laptop Power Bank" data-dimension48="Laptop Power Bank" data-dimension25="$42">View Deal</a></p></div><div class="product"><a data-dimension112="1369a5d2-980c-11f1-b6c9-639465ac0ca2" data-action="Deal Block" data-label="Lap Desk" data-dimension48="Lap Desk" data-dimension25="$39" href="https://www.amazon.com/LapGear-Office-wrist-mouse-holder/dp/B07N9L5934" 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:100.00%;"><img id="87rUDZEtRZ66J9maTPquc6" name="LAPGEAR Home Office Pro Lap Desk" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/87rUDZEtRZ66J9maTPquc6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The LapGear lap desk lets you keep up with your schoolwork from the comfort of your couch, bed, or anywhere else. It fits most 15.6-inch laptops and includes a cushioned wrist rest, a built-in mouse pad, and a phone slot. The dual-bolster microbead cushion keeps things cool and stable, while the lightweight design and carry handle make it easy to move around your dorm room.<a class="view-deal button" href="https://www.amazon.com/LapGear-Office-wrist-mouse-holder/dp/B07N9L5934" target="_blank" rel="nofollow" data-dimension112="1369a5d2-980c-11f1-b6c9-639465ac0ca2" data-action="Deal Block" data-label="Lap Desk" data-dimension48="Lap Desk" data-dimension25="$39">View Deal</a></p></div><div class="product"><a data-dimension112="1369a64a-980c-11f1-a3c8-f3fd19d1dcca" data-action="Deal Block" data-label="Laptop Cooling Pad" data-dimension48="Laptop Cooling Pad" data-dimension25="$25" href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1219px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="obsJA3XZiWtDcnvrMKkgEN" name="havit HV-F2056 15.6-17 Laptop Cooler Cooling Pad" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/obsJA3XZiWtDcnvrMKkgEN-1920-80.jpg" mos="" align="middle" fullscreen="" width="1219" height="1219" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow" data-dimension112="1369a64a-980c-11f1-a3c8-f3fd19d1dcca" data-action="Deal Block" data-label="Laptop Cooling Pad" data-dimension48="Laptop Cooling Pad" data-dimension25="$25">Laptop Cooling Pad: was $34 now $25</a></strong><br>Keep your laptop running cool with this slim, portable cooling pad from Havit. Three large 110mm fans deliver strong airflow at low noise levels while the metal mesh surface provides a stable, wear-resistant platform. Adjustable height settings improve ergonomics for typing or viewing, and extra USB ports add convenience. It's also thin and light enough to travel with easily.<a class="view-deal button" href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow" data-dimension112="1369a64a-980c-11f1-a3c8-f3fd19d1dcca" data-action="Deal Block" data-label="Laptop Cooling Pad" data-dimension48="Laptop Cooling Pad" data-dimension25="$25">View Deal</a></p></div><div class="product"><a data-dimension112="1369a6c2-980c-11f1-9970-ebe90793bfe4" data-action="Deal Block" data-label="Magnetic Laptop Privacy Screen" data-dimension48="Magnetic Laptop Privacy Screen" data-dimension25="$34" href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" 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:100.00%;"><img id="7ntmQ7UusVHY6e5bwvtdya" name="SightPro Magnetic Laptop Privacy Screen" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/7ntmQ7UusVHY6e5bwvtdya-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" target="_blank" rel="nofollow" data-dimension112="1369a6c2-980c-11f1-9970-ebe90793bfe4" data-action="Deal Block" data-label="Magnetic Laptop Privacy Screen" data-dimension48="Magnetic Laptop Privacy Screen" data-dimension25="$34">Magnetic Laptop Privacy Screen: $34</a></strong><br>Keep prying eyes off your screen with this magnetic privacy filter. You won't need adhesives, as you can simply snap it along the top of compatible 16-inch (16:10) laptops. Anyone viewing from the side will see only a black screen, while you get a clear, straight-on, glare-free view. This also comes with magnetic stickers, a webcam cover, a storage folder, and a cleaning cloth.<a class="view-deal button" href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" target="_blank" rel="nofollow" data-dimension112="1369a6c2-980c-11f1-9970-ebe90793bfe4" data-action="Deal Block" data-label="Magnetic Laptop Privacy Screen" data-dimension48="Magnetic Laptop Privacy Screen" data-dimension25="$34">View Deal</a></p></div><div class="product"><a data-dimension112="1369a76c-980c-11f1-8235-4325f8401eba" data-action="Deal Block" data-label="Travel Laptop Backpack" data-dimension48="Travel Laptop Backpack" data-dimension25="$18" href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1499px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="JPNbVDjAP9oC4f7krANcj" name="MATEIN Travel Laptop Backpack" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/JPNbVDjAP9oC4f7krANcj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1499" height="1499" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow" data-dimension112="1369a76c-980c-11f1-8235-4325f8401eba" data-action="Deal Block" data-label="Travel Laptop Backpack" data-dimension48="Travel Laptop Backpack" data-dimension25="$18">Travel Laptop Backpack: was $33 now $18</a></strong><br>This slim, sturdy backpack fits a 15.6-inch laptop and your peripherals. Multiple compartments keep chargers, notebooks, and accessories organized, while padded straps and a luggage strap make commuting or travel more comfortable. Side mesh pockets handle water bottles or umbrellas.<a class="view-deal button" href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow" data-dimension112="1369a76c-980c-11f1-8235-4325f8401eba" data-action="Deal Block" data-label="Travel Laptop Backpack" data-dimension48="Travel Laptop Backpack" data-dimension25="$18">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92906-980f-11f1-a9eb-1d1cc5855f9a" data-action="Deal Block" data-label="B35 Wireless Keyboard" data-dimension48="B35 Wireless Keyboard" data-dimension25="$39" href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" 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:100.00%;"><img id="rknpsbpWWxiG8kqNLucdkh" name="Keychron B35 Wireless Keyboard" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/rknpsbpWWxiG8kqNLucdkh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" target="_blank" rel="nofollow" data-dimension112="bdb92906-980f-11f1-a9eb-1d1cc5855f9a" data-action="Deal Block" data-label="B35 Wireless Keyboard" data-dimension48="B35 Wireless Keyboard" data-dimension25="$39">B35 Wireless Keyboard: $39</a></strong><br>The Keychron B35 is a big step up from a standard laptop keyboard. This model has a 96% layout with a numpad and tall switches, plus connectivity via Bluetooth (up to three devices) or the included 2.4GHz dongle. Its battery is rated for 320 hours, and the keyboard is compatible with Mac or Windows.<a class="view-deal button" href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" target="_blank" rel="nofollow" data-dimension112="bdb92906-980f-11f1-a9eb-1d1cc5855f9a" data-action="Deal Block" data-label="B35 Wireless Keyboard" data-dimension48="B35 Wireless Keyboard" data-dimension25="$39">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92992-980f-11f1-ad41-216931eb1e74" data-action="Deal Block" data-label="Laptop Sleeve" data-dimension48="Laptop Sleeve" data-dimension25="$13" href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1347px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="As2VFCng24n49bZgE8SLf8" name="MOSISO Laptop Case 13.3 inch" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/As2VFCng24n49bZgE8SLf8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1347" height="1347" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow" data-dimension112="bdb92992-980f-11f1-ad41-216931eb1e74" data-action="Deal Block" data-label="Laptop Sleeve" data-dimension48="Laptop Sleeve" data-dimension25="$13">Laptop Sleeve: was $14 now $13</a></strong><br>Keep your laptop safe with this slim, water-repellent polyester sleeve from Mosiso. The soft fleece lining and foam padding absorb shocks and prevent scratches, while the front vertical pocket holds a charger, mouse, or other accessories.<a class="view-deal button" href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow" data-dimension112="bdb92992-980f-11f1-ad41-216931eb1e74" data-action="Deal Block" data-label="Laptop Sleeve" data-dimension48="Laptop Sleeve" data-dimension25="$13">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92a14-980f-11f1-8ea1-8f40cee70415" data-action="Deal Block" data-label="Portable Monitor" data-dimension48="Portable Monitor" data-dimension25="$44" href="https://www.amazon.com/MNN-Portable-15-6inch-Ultra-Slim-Speakers/dp/B0B9NNWXVP" 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:100.00%;"><img id="W4Xc3du2WG7ySgAYEFrJEL" name="MNN 15.6 FHD 60Hz Portable Monitor" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/W4Xc3du2WG7ySgAYEFrJEL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>This MNN portable monitor lets you have a dual-screen setup wherever you are. One USB-C cable handles both power and video, and it also supports HDMI. Built-in speakers, a smart cover that doubles as a stand, and eye-care features make it great for schoolwork or even gaming. Under an inch thick and around 1.5 lbs, it slips easily into a bag.<a class="view-deal button" href="https://www.amazon.com/MNN-Portable-15-6inch-Ultra-Slim-Speakers/dp/B0B9NNWXVP" target="_blank" rel="nofollow" data-dimension112="bdb92a14-980f-11f1-8ea1-8f40cee70415" data-action="Deal Block" data-label="Portable Monitor" data-dimension48="Portable Monitor" data-dimension25="$44">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92a96-980f-11f1-badc-3db3ae1b7163" data-action="Deal Block" data-label="USB-C Docking Station" data-dimension48="USB-C Docking Station" data-dimension25="$36" href="https://www.amazon.com/Docking-Station-Monitor-Charging-Compatible/dp/B0DW85W3FV" 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:100.00%;"><img id="6GNDgWw4SoWz8rQzw3zGPZ" name="Acer USB-C Docking Station" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/6GNDgWw4SoWz8rQzw3zGPZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>This Acer USB-C docking station gives your laptop more versatility. You can connect external monitors, peripherals, and card readers. This docking station is perfect for multitasking at a desk or creating a clean dual-screen setup without cable clutter.<a class="view-deal button" href="https://www.amazon.com/Docking-Station-Monitor-Charging-Compatible/dp/B0DW85W3FV" target="_blank" rel="nofollow" data-dimension112="bdb92a96-980f-11f1-badc-3db3ae1b7163" data-action="Deal Block" data-label="USB-C Docking Station" data-dimension48="USB-C Docking Station" data-dimension25="$36">View Deal</a></p></div><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=laptops&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, .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("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vc2hvcC1hc3NldHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9zaG9wLWFzc2V0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9zaG9wLWFzc2V0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vc2hvcC1hc3NldHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9zaG9wLWFzc2V0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/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/computing/laptops/im-a-laptop-reviewer-12-essential-back-to-school-laptop-accessories-under-usd50-i-recommend</link>
                                                                            <description>
                            <![CDATA[ Students don't have to spend a fortune to get more use from their laptops. Here are 12 must-have back-to-school laptop accessories for under $50. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">LShMZvr56wbVC3Ud9pfY33</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/V8Whksrri437X4WLm946GV-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sun, 16 Aug 2026 09:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Laptops]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                <author><![CDATA[ tony.polanco@futurenet.com (Tony Polanco) ]]></author>                    <dc:creator><![CDATA[ Tony Polanco ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/atzRNqFt5wYgEUPBDahWsD-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ null ]]></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/V8Whksrri437X4WLm946GV-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[MacBook Neo shown on desk]]></media:description>                                                            <media:text><![CDATA[MacBook Neo shown on desk]]></media:text>
                                <media:title type="plain"><![CDATA[MacBook Neo shown on desk]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/V8Whksrri437X4WLm946GV-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The <a href="https://www.tomsguide.com/computing/laptops/best-laptops">best laptops</a> are a student’s most important ally, especially now during the back-to-school season. Though you can certainly make do with a laptop out of the box, you can make your notebook even more productive with the right accessories. Best of all, you don’t have to break the bank to get everything you’d need.</p><p>I haven’t been inside a classroom in decades, but I review laptops for a living and know the best items to recommend. This includes things like laptop stands, USB hubs, privacy screens, wireless peripherals, and more. Best of all, you can nab everything I’ve listed below for under $50 each, as being a student can already be pretty expensive.</p><p>To that end, here are the 12 best laptop accessories I recommend right now. I've also included the two <a href="https://www.tomsguide.com/best-picks/best-budget-laptops">best budget laptops</a> to get right now. Also, check out our <a href="https://www.tomsguide.com/news/ultimate-back-to-school-guide">Ultimate Back-to-School Guide </a>2026 for more deals. Okay, let's dive in!</p><h3 class="article-body__section" id="section-quick-links"><span>Quick Links</span></h3><ul><li><strong>Mosiso Laptop Sleeve: </strong><a href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow"><strong>was $14 now $13 @ Amazon</strong></a><strong></strong></li><li><strong>Matein Travel Laptop Backpack: </strong><a href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow"><strong>was $33 now $18 @ Amazon</strong></a><strong></strong></li><li><strong>Havit Laptop Cooling Pad: </strong><a href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow"><strong>was $34 now $25 @ Amazon</strong></a></li><li><strong>Lamicall Laptop Stand Riser: </strong><a href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow"><strong>was $35 now $29 @ Amazon</strong></a><strong></strong></li><li><strong>Logitech Signature M650: </strong><a href="https://www.amazon.com/dp/B09NSCZVB1" target="_blank" rel="nofollow"><strong>was $39 now $30 @ Amazon</strong></a><strong></strong></li><li><strong>SightPro Magnetic Laptop Privacy Screen: </strong><a href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" target="_blank" rel="nofollow"><strong>$34 @ Amazon</strong></a><strong></strong></li><li><strong>Acer USB-C Docking Station: </strong><a href="https://www.amazon.com/Docking-Station-Monitor-Charging-Compatible/dp/B0DW85W3FV" target="_blank" rel="nofollow"><strong>was $47 now $36 @ Amazon</strong></a></li><li><strong>Keychron B35 Wireless Keyboard: </strong><a href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" target="_blank" rel="nofollow"><strong>$39 @ Amazon</strong></a><strong></strong></li><li><strong>LapGear Lap Desk: </strong><a href="https://www.amazon.com/LapGear-Office-wrist-mouse-holder/dp/B07N9L5934" target="_blank" rel="nofollow"><strong>was $49 now $39 @ Amazon</strong></a></li><li><strong>UGreen 100W USB Multi-Power Charger: </strong><a href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" target="_blank" rel="nofollow"><strong>was $54 now $42 @ Amazon</strong></a><strong></strong></li><li><strong>Iniu Laptop Power Bank: </strong><a href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow"><strong>was $50 now $42 @ Amazon</strong></a><strong></strong></li><li><strong>MNN Portable Monitor: </strong><a href="https://www.amazon.com/MNN-Portable-15-6inch-Ultra-Slim-Speakers/dp/B0B9NNWXVP" target="_blank" rel="nofollow"><strong>was $49 now $44 @ Amazon</strong></a></li></ul><h3 class="article-body__section" id="section-the-two-best-budget-laptops"><span>The two best budget laptops</span></h3><p>If you're looking for an excellent budget laptop, the <a href="https://www.tomsguide.com/computing/macbooks/apple-macbook-neo-review">MacBook Neo</a> and new <a href="https://www.tomsguide.com/computing/laptops/dell-xps-13-2026-review">Dell XPS 13 (2026)</a> are both available starting at $699. So whether you're a macOS or Windows user, you won't be stuck with a subpar machine for school!</p><div class="product"><a data-dimension112="be0a445e-97f5-11f1-b67e-f9a7962276af" data-action="Deal Block" data-label="XPS 13 (2026)" data-dimension48="XPS 13 (2026)" data-dimension25="$699" href="https://www.dell.com/en-us/shop/dell-laptops/new-xps-13-laptop/spd/xps13dx13260laptop" 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:100.00%;"><img id="yVpTsWNESwCtxxLQmbPTgh" name="Dell XPS 13 (2026) deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/yVpTsWNESwCtxxLQmbPTgh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.dell.com/en-us/shop/dell-laptops/new-xps-13-laptop/spd/xps13dx13260laptop" target="_blank" rel="nofollow" data-dimension112="be0a445e-97f5-11f1-b67e-f9a7962276af" data-action="Deal Block" data-label="XPS 13 (2026)" data-dimension48="XPS 13 (2026)" data-dimension25="$699">XPS 13 (2026): $699</a></strong><br>The new Dell XPS 13 is a fantastic back-to-school laptop. This model has a 13.4-inch InfinityEdge 2.5K touch display, an Intel Core Series 3 "Wildcat Lake" CPU, 8GB of RAM, and a 512GB SSD. It's wrapped in an aluminum design that's just 0.5 inches thick and weighs 2.2 pounds.<a class="view-deal button" href="https://www.dell.com/en-us/shop/dell-laptops/new-xps-13-laptop/spd/xps13dx13260laptop" target="_blank" rel="nofollow" data-dimension112="be0a445e-97f5-11f1-b67e-f9a7962276af" data-action="Deal Block" data-label="XPS 13 (2026)" data-dimension48="XPS 13 (2026)" data-dimension25="$699">View Deal</a></p></div><div class="product"><a data-dimension112="be0a44d6-97f5-11f1-bbbf-112e2a81b715" data-action="Deal Block" data-label="13" MacBook Neo" data-dimension48="13" MacBook Neo" data-dimension25="$689" href="https://www.amazon.com/gp/aw/d/B0GR6FHGXX" 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:61.07%;"><img id="Lg4Dvg68j9SbB5CPNrTEpH" name="MacBook Neo deal block" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="916" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/gp/aw/d/B0GR6FHGXX" target="_blank" rel="nofollow" data-dimension112="be0a44d6-97f5-11f1-bbbf-112e2a81b715" data-action="Deal Block" data-label='13" MacBook Neo' data-dimension48='13" MacBook Neo' data-dimension25="$689">13" MacBook Neo: was $699 now $689</a></strong><br>The MacBook Neo is Apple's most affordable Mac. The $699 laptop sports a 13-inch Liquid Retina (2408 x 1506) display, an A18 Pro chipset, 8GB of RAM, a 256GB SSD, and a 1080p FaceTime HD camera. There's also the option to add Touch ID and a 512GB SSD for a total price of $799. The 2.7-lb. laptop is available in Indigo, Blush, Citrus, and Silver.<a class="view-deal button" href="https://www.amazon.com/gp/aw/d/B0GR6FHGXX" target="_blank" rel="nofollow" data-dimension112="be0a44d6-97f5-11f1-bbbf-112e2a81b715" data-action="Deal Block" data-label="13" MacBook Neo" data-dimension48="13" MacBook Neo" data-dimension25="$689">View Deal</a></p></div><h3 class="article-body__section" id="section-12-back-to-school-laptop-accessories"><span>12 back-to-school laptop accessories</span></h3><div class="product"><a data-dimension112="4a88e4fc-97f7-11f1-9f6f-ff71000c42b7" data-action="Deal Block" data-label="Adjustable Laptop Stand" data-dimension48="Adjustable Laptop Stand" data-dimension25="$29" href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:979px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="uBkMygLLvTZMUbpkEm4kbG" name="lamicallgrid1.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/uBkMygLLvTZMUbpkEm4kbG-1920-80.jpg" mos="" align="middle" fullscreen="" width="979" height="979" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow" data-dimension112="4a88e4fc-97f7-11f1-9f6f-ff71000c42b7" data-action="Deal Block" data-label="Adjustable Laptop Stand" data-dimension48="Adjustable Laptop Stand" data-dimension25="$29">Adjustable Laptop Stand: was $35 now $29</a></strong><br>Lamicall's laptop stand is the best overall because it offers plenty of height and angle adjustments, a built-in heat vent design, and large rubber grips with plenty of friction to keep your laptop steady while typing. Plus, it’s foldable.<a class="view-deal button" href="https://www.amazon.com/Lamicall-Laptop-Stand-Riser-Portable/dp/B08M94BTYC" target="_blank" rel="nofollow" data-dimension112="4a88e4fc-97f7-11f1-9f6f-ff71000c42b7" data-action="Deal Block" data-label="Adjustable Laptop Stand" data-dimension48="Adjustable Laptop Stand" data-dimension25="$29">View Deal</a></p></div><div class="product"><a data-dimension112="4a88e57e-97f7-11f1-880f-17895fcddf05" data-action="Deal Block" data-label="Nexode 100W GaN USB C Charger" data-dimension48="Nexode 100W GaN USB C Charger" data-dimension25="$42" href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" 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:100.00%;"><img id="JAPBAVnHQsSe34eMHtrMN6" name="Ugreen Nexode 100W charger.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/JAPBAVnHQsSe34eMHtrMN6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" target="_blank" rel="nofollow" data-dimension112="4a88e57e-97f7-11f1-880f-17895fcddf05" data-action="Deal Block" data-label="Nexode 100W GaN USB C Charger" data-dimension48="Nexode 100W GaN USB C Charger" data-dimension25="$42">Nexode 100W GaN USB C Charger: was $54 now $42</a></strong><br>The Ugreen Nexode 100W has three USB ports for charging your electronic devices, including tablets, smartphones, and even laptops. This is a great product for anyone with multiple electronic devices.<a class="view-deal button" href="https://www.amazon.com/UGREEN-100W-USB-Multiport-Charger/dp/B091Z6JNX4" target="_blank" rel="nofollow" data-dimension112="4a88e57e-97f7-11f1-880f-17895fcddf05" data-action="Deal Block" data-label="Nexode 100W GaN USB C Charger" data-dimension48="Nexode 100W GaN USB C Charger" data-dimension25="$42">View Deal</a></p></div><div class="product"><a data-dimension112="4b2dc48a-97f8-11f1-b5f2-d72e9c789050" data-action="Deal Block" data-label="Signature M650" data-dimension48="Signature M650" data-dimension25="$30" href="https://www.amazon.com/dp/B09NSCZVB1" 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:100.00%;"><img id="5eKWVc6aPV8Y3XZhwwnft4" name="Logitech Signature M650" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/5eKWVc6aPV8Y3XZhwwnft4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Perfect for casual clickers or those who don’t need to fork out for specialized ergonomic mice, the Logitech Signature M650 is a great all-rounder with two connection options and customizable shortcuts. However, it may not be comfortable enough for long term professional use.<a class="view-deal button" href="https://www.amazon.com/dp/B09NSCZVB1" target="_blank" rel="nofollow" data-dimension112="4b2dc48a-97f8-11f1-b5f2-d72e9c789050" data-action="Deal Block" data-label="Signature M650" data-dimension48="Signature M650" data-dimension25="$30">View Deal</a></p></div><div class="product"><a data-dimension112="da30bf74-97f9-11f1-a978-95a599440651" data-action="Deal Block" data-label="Laptop Power Bank" data-dimension48="Laptop Power Bank" data-dimension25="$42" href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1209px;"><p class="vanilla-image-block" style="padding-top:124.07%;"><img id="pqupwK4PpvFWLZssdRV3zV" name="INIU P62" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/pqupwK4PpvFWLZssdRV3zV-1920-80.jpg" mos="" align="middle" fullscreen="" width="1209" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow" data-dimension112="da30bf74-97f9-11f1-a978-95a599440651" data-action="Deal Block" data-label="Laptop Power Bank" data-dimension48="Laptop Power Bank" data-dimension25="$42">Laptop Power Bank: was $50 now $42</a></strong><br>This power bank delivers up to 65W high-speed charging for two devices at once. The 20,000mAh power bank packs INIU's high-density TinyCell and groundbreaking HyperStack technology, which is perfect if you don't like traveling with heavy bricks.<a class="view-deal button" href="https://www.amazon.com/INIU-20000mAh-Portable-Charging-3-Output/dp/B0DB86W481" target="_blank" rel="nofollow" data-dimension112="da30bf74-97f9-11f1-a978-95a599440651" data-action="Deal Block" data-label="Laptop Power Bank" data-dimension48="Laptop Power Bank" data-dimension25="$42">View Deal</a></p></div><div class="product"><a data-dimension112="1369a5d2-980c-11f1-b6c9-639465ac0ca2" data-action="Deal Block" data-label="Lap Desk" data-dimension48="Lap Desk" data-dimension25="$39" href="https://www.amazon.com/LapGear-Office-wrist-mouse-holder/dp/B07N9L5934" 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:100.00%;"><img id="87rUDZEtRZ66J9maTPquc6" name="LAPGEAR Home Office Pro Lap Desk" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/87rUDZEtRZ66J9maTPquc6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The LapGear lap desk lets you keep up with your schoolwork from the comfort of your couch, bed, or anywhere else. It fits most 15.6-inch laptops and includes a cushioned wrist rest, a built-in mouse pad, and a phone slot. The dual-bolster microbead cushion keeps things cool and stable, while the lightweight design and carry handle make it easy to move around your dorm room.<a class="view-deal button" href="https://www.amazon.com/LapGear-Office-wrist-mouse-holder/dp/B07N9L5934" target="_blank" rel="nofollow" data-dimension112="1369a5d2-980c-11f1-b6c9-639465ac0ca2" data-action="Deal Block" data-label="Lap Desk" data-dimension48="Lap Desk" data-dimension25="$39">View Deal</a></p></div><div class="product"><a data-dimension112="1369a64a-980c-11f1-a3c8-f3fd19d1dcca" data-action="Deal Block" data-label="Laptop Cooling Pad" data-dimension48="Laptop Cooling Pad" data-dimension25="$25" href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1219px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="obsJA3XZiWtDcnvrMKkgEN" name="havit HV-F2056 15.6-17 Laptop Cooler Cooling Pad" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/obsJA3XZiWtDcnvrMKkgEN-1920-80.jpg" mos="" align="middle" fullscreen="" width="1219" height="1219" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow" data-dimension112="1369a64a-980c-11f1-a3c8-f3fd19d1dcca" data-action="Deal Block" data-label="Laptop Cooling Pad" data-dimension48="Laptop Cooling Pad" data-dimension25="$25">Laptop Cooling Pad: was $34 now $25</a></strong><br>Keep your laptop running cool with this slim, portable cooling pad from Havit. Three large 110mm fans deliver strong airflow at low noise levels while the metal mesh surface provides a stable, wear-resistant platform. Adjustable height settings improve ergonomics for typing or viewing, and extra USB ports add convenience. It's also thin and light enough to travel with easily.<a class="view-deal button" href="https://www.amazon.com/HV-F2056-15-6-17-Laptop-Cooler-Cooling/dp/B00NNMB3KS" target="_blank" rel="nofollow" data-dimension112="1369a64a-980c-11f1-a3c8-f3fd19d1dcca" data-action="Deal Block" data-label="Laptop Cooling Pad" data-dimension48="Laptop Cooling Pad" data-dimension25="$25">View Deal</a></p></div><div class="product"><a data-dimension112="1369a6c2-980c-11f1-9970-ebe90793bfe4" data-action="Deal Block" data-label="Magnetic Laptop Privacy Screen" data-dimension48="Magnetic Laptop Privacy Screen" data-dimension25="$34" href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" 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:100.00%;"><img id="7ntmQ7UusVHY6e5bwvtdya" name="SightPro Magnetic Laptop Privacy Screen" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/7ntmQ7UusVHY6e5bwvtdya-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" target="_blank" rel="nofollow" data-dimension112="1369a6c2-980c-11f1-9970-ebe90793bfe4" data-action="Deal Block" data-label="Magnetic Laptop Privacy Screen" data-dimension48="Magnetic Laptop Privacy Screen" data-dimension25="$34">Magnetic Laptop Privacy Screen: $34</a></strong><br>Keep prying eyes off your screen with this magnetic privacy filter. You won't need adhesives, as you can simply snap it along the top of compatible 16-inch (16:10) laptops. Anyone viewing from the side will see only a black screen, while you get a clear, straight-on, glare-free view. This also comes with magnetic stickers, a webcam cover, a storage folder, and a cleaning cloth.<a class="view-deal button" href="https://www.amazon.com/SightPro-Magnetic-Laptop-Privacy-Screen/dp/B0D7LVVK64" target="_blank" rel="nofollow" data-dimension112="1369a6c2-980c-11f1-9970-ebe90793bfe4" data-action="Deal Block" data-label="Magnetic Laptop Privacy Screen" data-dimension48="Magnetic Laptop Privacy Screen" data-dimension25="$34">View Deal</a></p></div><div class="product"><a data-dimension112="1369a76c-980c-11f1-8235-4325f8401eba" data-action="Deal Block" data-label="Travel Laptop Backpack" data-dimension48="Travel Laptop Backpack" data-dimension25="$18" href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1499px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="JPNbVDjAP9oC4f7krANcj" name="MATEIN Travel Laptop Backpack" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/JPNbVDjAP9oC4f7krANcj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1499" height="1499" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow" data-dimension112="1369a76c-980c-11f1-8235-4325f8401eba" data-action="Deal Block" data-label="Travel Laptop Backpack" data-dimension48="Travel Laptop Backpack" data-dimension25="$18">Travel Laptop Backpack: was $33 now $18</a></strong><br>This slim, sturdy backpack fits a 15.6-inch laptop and your peripherals. Multiple compartments keep chargers, notebooks, and accessories organized, while padded straps and a luggage strap make commuting or travel more comfortable. Side mesh pockets handle water bottles or umbrellas.<a class="view-deal button" href="https://www.amazon.com/Backpack-Business-Charging-Resistant-Computer/dp/B06XZTZ7GB" target="_blank" rel="nofollow" data-dimension112="1369a76c-980c-11f1-8235-4325f8401eba" data-action="Deal Block" data-label="Travel Laptop Backpack" data-dimension48="Travel Laptop Backpack" data-dimension25="$18">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92906-980f-11f1-a9eb-1d1cc5855f9a" data-action="Deal Block" data-label="B35 Wireless Keyboard" data-dimension48="B35 Wireless Keyboard" data-dimension25="$39" href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" 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:100.00%;"><img id="rknpsbpWWxiG8kqNLucdkh" name="Keychron B35 Wireless Keyboard" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/rknpsbpWWxiG8kqNLucdkh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" target="_blank" rel="nofollow" data-dimension112="bdb92906-980f-11f1-a9eb-1d1cc5855f9a" data-action="Deal Block" data-label="B35 Wireless Keyboard" data-dimension48="B35 Wireless Keyboard" data-dimension25="$39">B35 Wireless Keyboard: $39</a></strong><br>The Keychron B35 is a big step up from a standard laptop keyboard. This model has a 96% layout with a numpad and tall switches, plus connectivity via Bluetooth (up to three devices) or the included 2.4GHz dongle. Its battery is rated for 320 hours, and the keyboard is compatible with Mac or Windows.<a class="view-deal button" href="https://www.amazon.com/Keychron-Wireless-Bluetooth-Ergonomic-Compatible/dp/B0FZ9FPX1T" target="_blank" rel="nofollow" data-dimension112="bdb92906-980f-11f1-a9eb-1d1cc5855f9a" data-action="Deal Block" data-label="B35 Wireless Keyboard" data-dimension48="B35 Wireless Keyboard" data-dimension25="$39">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92992-980f-11f1-ad41-216931eb1e74" data-action="Deal Block" data-label="Laptop Sleeve" data-dimension48="Laptop Sleeve" data-dimension25="$13" href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1347px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="As2VFCng24n49bZgE8SLf8" name="MOSISO Laptop Case 13.3 inch" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/As2VFCng24n49bZgE8SLf8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1347" height="1347" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow" data-dimension112="bdb92992-980f-11f1-ad41-216931eb1e74" data-action="Deal Block" data-label="Laptop Sleeve" data-dimension48="Laptop Sleeve" data-dimension25="$13">Laptop Sleeve: was $14 now $13</a></strong><br>Keep your laptop safe with this slim, water-repellent polyester sleeve from Mosiso. The soft fleece lining and foam padding absorb shocks and prevent scratches, while the front vertical pocket holds a charger, mouse, or other accessories.<a class="view-deal button" href="https://www.amazon.com/MOSISO-Polyester-Vertical-Repellent-Compatible/dp/B01N6M1Y9N" target="_blank" rel="nofollow" data-dimension112="bdb92992-980f-11f1-ad41-216931eb1e74" data-action="Deal Block" data-label="Laptop Sleeve" data-dimension48="Laptop Sleeve" data-dimension25="$13">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92a14-980f-11f1-8ea1-8f40cee70415" data-action="Deal Block" data-label="Portable Monitor" data-dimension48="Portable Monitor" data-dimension25="$44" href="https://www.amazon.com/MNN-Portable-15-6inch-Ultra-Slim-Speakers/dp/B0B9NNWXVP" 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:100.00%;"><img id="W4Xc3du2WG7ySgAYEFrJEL" name="MNN 15.6 FHD 60Hz Portable Monitor" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/W4Xc3du2WG7ySgAYEFrJEL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>This MNN portable monitor lets you have a dual-screen setup wherever you are. One USB-C cable handles both power and video, and it also supports HDMI. Built-in speakers, a smart cover that doubles as a stand, and eye-care features make it great for schoolwork or even gaming. Under an inch thick and around 1.5 lbs, it slips easily into a bag.<a class="view-deal button" href="https://www.amazon.com/MNN-Portable-15-6inch-Ultra-Slim-Speakers/dp/B0B9NNWXVP" target="_blank" rel="nofollow" data-dimension112="bdb92a14-980f-11f1-8ea1-8f40cee70415" data-action="Deal Block" data-label="Portable Monitor" data-dimension48="Portable Monitor" data-dimension25="$44">View Deal</a></p></div><div class="product"><a data-dimension112="bdb92a96-980f-11f1-badc-3db3ae1b7163" data-action="Deal Block" data-label="USB-C Docking Station" data-dimension48="USB-C Docking Station" data-dimension25="$36" href="https://www.amazon.com/Docking-Station-Monitor-Charging-Compatible/dp/B0DW85W3FV" 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:100.00%;"><img id="6GNDgWw4SoWz8rQzw3zGPZ" name="Acer USB-C Docking Station" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/6GNDgWw4SoWz8rQzw3zGPZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>This Acer USB-C docking station gives your laptop more versatility. You can connect external monitors, peripherals, and card readers. This docking station is perfect for multitasking at a desk or creating a clean dual-screen setup without cable clutter.<a class="view-deal button" href="https://www.amazon.com/Docking-Station-Monitor-Charging-Compatible/dp/B0DW85W3FV" target="_blank" rel="nofollow" data-dimension112="bdb92a96-980f-11f1-badc-3db3ae1b7163" data-action="Deal Block" data-label="USB-C Docking Station" data-dimension48="USB-C Docking Station" data-dimension25="$36">View Deal</a></p></div><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=laptops&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, .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("LCwxLDIsMyw0LDUsNiw3LDgsOSwxMCwxMSwxMiwxMywxNCwxNQ0KUm91bmRlbCB0ZXh0LEFsbCxUVnMsRm9vdHdlYXIsQXBwYXJlbCxNYXR0cmVzZXMsQXBwbGlhbmNlcyxXZWFyYWJsZSB0ZWNoLEhlYWRwaG9uZXMsU21hcnQgSG9tZSxTcGVha2VycyxMYXB0b3BzLFRhYmxldHMsQ29tcHV0aW5nLFBob25lcyxHYW1pbmcsTGVnbw0KUm91bmRlbCBpbWFnZSxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL2FpLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL3R2cy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvN2IzYTIyNGIwNzk2M2M2MjdiNmI5MDliZDc4MzM4MzZlMDJmZjgxOS5qcGcud2VicCxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy84NGRhYzVkNDhlZDJkNDQ4NTU5ZWJhNjdhY2U4MzE0Y2M2N2NjZDk0LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vc2hvcC1hc3NldHMvY2Fyb3VzZWwvbWF0dHJlc3Nlcy5wbmcsaHR0cHM6Ly9pbWFnZXMuZmllLmZ1dHVyZWNkbi5uZXQvcHJvZHVjdHMvNzY4ZTk3Y2ViMDcxODAxZmFlMjA5MTBkMDgyMGIxNmY3NDdhZjkzOS5qcGcud2VicCxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL3dlbGxuZXNzLnBuZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL2hlYWRwaG9uZXMuanBnLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzg5NTM1YmVlYmUyMGRiYmQ0YTM0NmQ2ZDZiZGZlOTFkOGE4ODRhMjEuanBnLndlYnAsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9zaG9wLWFzc2V0cy9jYXJvdXNlbC9hdWRpby5qcGcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9zaG9wLWFzc2V0cy9jYXJvdXNlbC9sYXB0b3BzLmpwZyxodHRwczovL2ltYWdlcy5maWUuZnV0dXJlY2RuLm5ldC9wcm9kdWN0cy8yMzk3NTY0ZWQ3YTVmZjk0N2U5YjZiMzBlNTRmNDc0OTRiODQxZjg5LmpwZy53ZWJwLGh0dHBzOi8vd3d3LnRvbXNndWlkZS5jb20vc2hvcC1hc3NldHMvY2Fyb3VzZWwvY29tcHV0aW5nLmpwZyxodHRwczovL3d3dy50b21zZ3VpZGUuY29tL3Nob3AtYXNzZXRzL2Nhcm91c2VsL3Bob25lcy5wbmcsaHR0cHM6Ly93d3cudG9tc2d1aWRlLmNvbS9zaG9wLWFzc2V0cy9jYXJvdXNlbC9nYW1pbmcucG5nLGh0dHBzOi8vaW1hZ2VzLmZpZS5mdXR1cmVjZG4ubmV0L3Byb2R1Y3RzLzRmNmM2MjFjYWMwYmMxYTg1ZDU5M2UzNTk0YmE1YjM0OWVmZmQyOTIuanBnLndlYnANClNlYXJjaCBRdWVyeSxFdmVyeXRoaW5nLFRlbGV2aXNpb25zLCJTbmVha2VycywgcnVubmluZyBzaG9lcywgc2FuZGFscyIsQ2xvdGhpbmcsTWF0dHJlc3NlcyxIb21lIEFwcGxpYW5jZXMsV2VhcmFibGVzICYgRml0bmVzcyBUZWNoLEhlYWRwaG9uZXMsSG9tZSBUZWNoLFNwZWFrZXJzLExhcHRvcHMsVGFibGV0cyxDb21wdXRpbmcsUGhvbmVzLEdhbWluZyxDb25zdHJ1Y3Rpb24gVG95cw0KRGlzY291bnQgQW1vdW50LG1pbiA1JSxtaW4gMTAlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUsbWluIDUlLG1pbiA1JSxtaW4gNSUNClByaWNlIFJhbmdlLCwsLCxtaW4gJDQwMCwsLCxtaW4gJDI1LCxtaW4gJDMwMCwsLG1pbiAkMTAwLCwNCkJyYW5kIFNlbGVjdGlvbiwsLCwsLCwsLCwsLCwsLCwNCkZpbHRlciBidXR0b25zLCwsLCwsLCwsLCwsLCwsLA0KMSxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMsTGlnaHRuaW5nIGRlYWxzLExpZ2h0bmluZyBkZWFscyxMaWdodG5pbmcgZGVhbHMNCjIsQW1hem9uIGRlYWxzLFVuZGVyICQxMDAwLDUwJSBvZmYsQWRpZGFzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsNTAlIG9mZixBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscyxBbWF6b24gZGVhbHMsQW1hem9uIGRlYWxzLEFtYXpvbiBkZWFscw0KMyxPdmVyICQ0MDAsVW5kZXIgJDUwMCxIb2thLE5pa2UsU2FhdHZhLE5pbmphLDQwJSBvZmYsSkxhYiwsSkJMLERlbGwsLEFzdXMsQXBwbGUsQ29uc29sZXMsU3RhciBXYXJzDQo0LFVuZGVyICQxMDAwLDUwJSBvZmYsU2tlY2hlcnMsVW5kZXIgQXJtb3VyLEhlbGl4LFNoYXJrLEdhcm1pbixBbmtlciBTb3VuZGNvcmUsUmluZyxTb25vcyxBcHBsZSxBcHBsZSxUUC1saW5rLFNhbXN1bmcsQWNjZXNzb3JpZXMsVW5kZXIgJDI1DQo1LFVuZGVyICQ1MDAsTEcsQXNpY3MsQ29sdW1iaWEsRHJlYW1DbG91ZCxLZXVyaWcsQXBwbGUsU29ueSxHb3ZlZSxUcmliaXQsTGVub3ZvLFNhbXN1bmcsRWVybyxHb29nbGUsR2FtZXMsVW5kZXIgJDUwDQo2LDUwJSBvZmYsU2Ftc3VuZyxOaWtlLFBhdGFnb25pYSxOZWN0YXIsRGUnTG9uZ2hpLEFtYXpmaXQsQXBwbGUsS2FzYSBzbWFydCxTb255LEFsaWVud2FyZSxUQ0wsTmV0Z2VhcixNb3Rvcm9sYSxOaW50ZW5kbyxCb3RhbmljYWxzDQo3LEFtYXpvbixIaXNlbnNlLE5ldyBCYWxhbmNlLEFyYyd0ZXJ5eCxUZW1wdXItcGVkaWMsRHlzb24sRml0Yml0LEJlYXRzLFBoaWxpcHMgSHVlLEFua2VyLEFjZXIsT25lUGx1cyxEZWxsLE9uZVBsdXMsU29ueSxEaXNuZXkNCjgsQXBwbGUsVENMLEFkaWRhcyxDYXJoYXJ0dCxCZWFyLEJpc3NlbGwsU2Ftc3VuZyxFYXJmdW4sQmxpbmssQmVhdHMsTVNJLE1pY3Jvc29mdCxBY2VyLE5vdGhpbmcsWGJveCxNYXJ2ZWwNCjksLFNvbnksU2F1Y29ueSxUaGUgTm9ydGggRmFjZSxTaWVuYSxOdXRyaWJ1bGxldCxPdXJhLFNhbXN1bmcsR29vZ2xlIE5lc3QgLE1hcnNoYWxsLFNhbXN1bmcsTGVub3ZvLExlbm92bywsLFBva2Vtb24NCjEwLCxSb2t1LEJpcmtlbnN0b2NrLENSWiBZb2dhLFdpbmtCZWRzLEJsYWNrIGFuZCBEZWNrZXIsUmluZ2Nvbm4sQ01GLEV1ZnksU2Ftc3VuZyxNaWNyb3NvZnQsUmVNYXJrYWJsZSxBbGllbndhcmUsLCwNCjExLCwsQnJvb2tzLFRoZSBHeW0gUGVvcGxlLEJyb29rbHluIGJlZGRpbmcsTmVzcHJlc3NvLCwxTW9yZSxBcmxvLCxSYXplciwsQ29yc2FpciwsLA0KMTIsLCxDcm9jcywsRWlnaHQgU2xlZXAsQ3Vpc2luYXJ0LCxKQkwsLCwsLEhQLCwsDQpOb3RlcywsLCwsLCwsLCwsLCwsLCwNCiwsIlByaW9yaXRpc2UgYmlnZ2VzdCAlLyQgZGlzY291bnQsIFR2cyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiB0aGUgbW9zdCBwb3B1bGFyIGV2ZW4gaWYgdGhleSBhcmUgc3RpbGwgZXhwZW5zaXZlIiwiTm8gcGF0dGVybiB0byBwcmljaW5nL2Rpc2NvdW50LCByZWFkZXJzIG1haW5seSBzaG9wIGJ5IGJyYW5kL3JlY29nbmlzYWJsZSBzaG9lcyIsIk5vIHBhdHRlcm4gdG8gcHJpY2luZy9kaXNjb3VudCwgcmVhZGVycyBtYWlubHkgc2hvcCBieSBicmFuZCIsIkEgbGFiZWwgd2lsbCBkZWZpbml0ZWx5IGhlbHAgaGVyZSBlLmcuIGJlc3QgZm9yIHNpZGUgc2xlZXBlciwgYmVzdCBtZW1vcnkgZm9hbSIsIkFwcGxpYW5jZXMgaXMgYSBiaWcgY2F0ZWdvcnksIGlzIGl0IHBvc3NpYmxlIHRvIHNwbGl0IGludG8ga2l0Y2hlbiBhcHBsaWFuY2VzLCBmbG9vcmNhcmUsIGFpciBoZWFsdGgvY29vbGluZz8gT3Igc2ltaWxhciIsIkZvY3VzIG9uIHZhbHVlIGZvciBtb25leSwgR2FybWlucyB3aXRoIH41MCUgb2ZmIGhhdmUgYmVlbiBwb3B1bGFyIGV2ZW4gdGhvdWdoIHRoZXkgYXJlIHN0aWxsICQ1MDAiLCwsLCxJbmNsdWRlIEtpbmRsZXMsSSB3b3VsZCBpbmNsdWRlIHdpZmkgcm91dGVycyBoZXJlIGluc3RlYWQgb2Ygc21hcnQgaG9tZSxDYW4gd2Ugc3VyZmFjZSBwaG9uZSBwcm92aWRlciBkZWFscz8gVC1tb2JpbGUgYW5kIHZlcml6b24gd291bGQgbWFrZSBhIGxvdCBtb3JlIG1vbmV5IHRoYW4gQW1hem9uLCwNCiwsaGF2aW5nIGEgJ2Jlc3QgZm9yJyBsYWJlbCB3b3VsZCBiZSBoZWxwZnVsIGUuZy4gYmVzdCBmb3IgYnJpZ2h0IHJvb20sQ2FuIHdlIHN0b3Aga2lkcyBzaG9lcyBmcm9tIHB1bGxpbmcgdGhyb3VnaD8sIldpbGwgdGhpcyBpbmNsdWRlIGFjY2Vzc29yaWVzIGUuZy4gY2FwcywgYmFncywgaWYgc28gbWFrZSBzdXJlIHRoZXNlIGFyZSBtaXhlZCB0aHJvdWdob3V0IGNsb3RoaW5nIGRlYWxzIixXaWxsIHRoaXMgaW5jbHVkZSB0b3BwZXJzIGFuZCBwaWxsb3dzPyBTZWVpbmcgbW9yZSBtb21lbnR1bSB3aXRoIHRoaXMgY2F0ZWdvcnkgcmVjZW50bHkgc28gYSBiZWRkaW5nIHRhYiBtaWdodCB3b3JrLCwiTmVlZCB0byBtYWtlIHN1cmUgYmFuZHMsIHNjcmVlbiBwcm90ZWN0b3JzIGV0Yy4gZG9uJ3QgcHVsbCBpbnRvIGhlcmUiLCwsLCwsLCwsDQosLCJQcmlvcml0aXNlIDY1JycgYW5kIDU1JyBpbmNoIFRWcywgdGhlbiBiaWdnZXIgc2NyZWVucyBiZWZvcmUgdGhlIHNtYWxsZXIgc2l6ZXMiLCwsUXVlZW4gaXMgdGhlIG1vc3QgcG9wdWxhciBzaXplIGluIHRoZSBVUyAtIHByaW9yaXRpc2UgZGVhbHMgZm9yIHRoaXMgc2l6ZSwsLCwsLCwsLCwsDQosLCwsLCwsLCwsLCwsLCwsDQpDYXRlZ29yaWVzIHRvIGNvbnNpZGVyLCxQcm9kdWN0cyBpbmNsdWRlZCwsLCwsLCwsLCwsLCwsDQpVbmRlciAkNTA/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[ Dell Pro 7 Webcam review ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Depending on your home-office needs, the Dell Pro 7 Webcam could be the <a href="https://www.tomsguide.com/computing/peripherals/best-webcams">best webcam</a> to add to your video conferencing setup. The easy setup means you can have it up and running in seconds, and the auto-adjustments based on AI-powered auto adjustments help you stay looking professional. The microphone quality is good, and the privacy lens is helpful for quickly switching off your camera. </p><p>But it’s worth keeping in mind that the mount and the camera are heavier than most others on the market, and the color replication is poor. I also think you can get better for the price tag. </p><p>To find out more, you can keep reading this full Dell Pro 7 Webcam review.</p><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-cheat-sheet"><span>Dell Pro 7 Webcam review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 4K webcam with smart modes for auto-adjustment</li><li><strong>Who is it for?</strong> At-home office workers who want to upgrade their setup</li><li><strong>What does it cost?</strong> The Dell Pro 7 Webcam is available for <a href="https://www.dell.com/en-us/shop/dell-pro-7-webcam-4k-wb726/apd/319-bbmt/pc-accessories" target="_blank" rel="nofollow">$199 / £190</a> from the Dell online store</li><li><strong>What do we like?</strong> It’s easy to set up, automatically adjusts exposure and white balance, has good microphone quality and a privacy lens</li><li><strong>What don’t we like?</strong> The mount is very heavy and the color replication is poor</li></ul><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-specs"><span>Dell Pro 7 Webcam review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.dell.com/en-us/shop/dell-pro-7-webcam-4k-wb726/apd/319-bbmt/pc-accessories" target="_blank" rel="nofollow">$199</a> / <a href="https://www.dell.com/en-uk/shop/dell-pro-7-webcam-4k-wb726/apd/722-bbcv/pc-accessories" target="_blank" rel="nofollow">£190</a></p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>2.6 x 2.1 x 1.4 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>8.5 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Maximum monitor thickness support</strong></p></td><td  ><p>1.8 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired </p></td></tr><tr><td class="firstcol " ><p><strong>Maximum video resolution</strong></p></td><td  ><p>4K (3840 x 2160)</p></td></tr><tr><td class="firstcol " ><p><strong>Maximum frame rate </strong></p></td><td  ><p>60fps</p></td></tr><tr><td class="firstcol " ><p><strong>Digital zoom</strong></p></td><td  ><p>x4</p></td></tr><tr><td class="firstcol " ><p><strong>Supported video modes</strong></p></td><td  ><p>2160p, 1080p, 720p</p></td></tr><tr><td class="firstcol " ><p><strong>Image processing technology</strong></p></td><td  ><p>Advanced AI-Imaging with Smart Enhancement</p></td></tr><tr><td class="firstcol " ><p><strong>Maximum aperture</strong></p></td><td  ><p>f/2.0</p></td></tr><tr><td class="firstcol " ><p><strong>Microphone range</strong></p></td><td  ><p>Up to 10 feet</p></td></tr><tr><td class="firstcol " ><p><strong>Supported operating systems</strong></p></td><td  ><p>Windows 11, macOS 14 or higher</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-the-ups"><span>Dell Pro 7 Webcam review: The ups</span></h2><p>The Dell Pro 7 Webcam has a lot to like. With its effortless setup and intelligent features, it’s a breeze to integrate into your existing work routine. </p><h2 id="easy-to-set-up">Easy to set up</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Lp7rgyDUNBv4DJFDzRDTt8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/Lp7rgyDUNBv4DJFDzRDTt8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam with your computer couldn’t be easier (once you balance it, but more on that later). The camera comes with a 10-foot USB-C cable, plus a USB-A converter for compatibility with older devices. Simply plug this into your computer, and you’re done. </p><p>I had to switch from my built-in laptop webcam to the external one, but you won’t need to do this if you’re using a PC. I did the same for the microphone to make sure I was using the Dell camera’s mic. </p><h2 id="intelligent-features">Intelligent 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="ku2bP3gLWHLoeP3XirEYv8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ku2bP3gLWHLoeP3XirEYv8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam offers a few intelligent features to make things a little easier. The camera uses Advanced AI-Imaging with Smart Enhancement, but this is just a fancy way of saying that it considers your face, environment, and movements to apply the best exposure settings to the camera. </p><p>It automatically adjusts exposure and white balance using Smart Scene Detection, which mostly worked well. However, other webcams, like the <a href="https://www.tomsguide.com/computing/peripherals/logitech-mx-brio-705-for-business-webcam-review">Logitech MX BRIO 750</a> for $199, perform better in low-light conditions. </p><p>If you want something a bit more high-tech for presenting, with impressive gimbal tracking and whiteboard focus settings, you should look at the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2-pro-review">Insta360 Link 2 Pro</a> ($249). It’s a little more expensive, but it opens up a whole new level of possibility when taking work calls and presenting ideas. </p><h2 id="good-microphone">Good microphone</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eWmZvy53FPUNerBjV3fV58" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/eWmZvy53FPUNerBjV3fV58-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 on-board microphone of the Dell Pro 7 Webcam replicated my voice clearly. I sounded slightly crisper than when I use my <a href="https://www.tomsguide.com/reviews/apple-airpods-3">Apple AirPods 3</a>. It effectively reduced ambient sound around me and didn’t pick up any typing sounds from my keyboard. </p><h2 id="privacy-lens">Privacy lens</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dnuRa4psKvvMKiMCTvxLu8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/dnuRa4psKvvMKiMCTvxLu8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam has a built-in privacy lens that is controlled by a switch at the top. This quickly shuts off the camera view during meetings, so you don’t need to fiddle with settings on your computer.</p><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-the-downs"><span>Dell Pro 7 Webcam review: The downs</span></h2><p>While the Dell Pro 7 Webcam has a lot to love, there are a few things that put me off. The color replication in particular made my saturation-loving self shy away from this camera. </p><h2 id="heavy-mount">Heavy mount </h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NFZSmpAGNqyvUhd4FHq4N8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/NFZSmpAGNqyvUhd4FHq4N8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam mount is very heavy and is made even heavier by the camera. This meant that I found it difficult to balance, and it was actually pulling down my laptop screen. </p><p>The effect of this isn’t as bad if you’re mounting it to a larger monitor, but it still makes it tricky during the initial setup to get the right balance. The whole unit weighs 8.5 ounces. In comparison, the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-pro-review">Insta360 Link 2C Pro</a> (with more capabilities) weighs just 4 ounces, while the <a href="https://www.tomsguide.com/computing/peripherals/opal-tadpole-webcam-review-desktop-users-must-keep-kissing-frogs">Opal Tadpole</a> weighs just 1.2 ounces but only works with laptops.</p><h2 id="color-replication">Color replication</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="58JvjVAPqLzHSfEGyVMxC8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/58JvjVAPqLzHSfEGyVMxC8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam made me look gaunt. Haunting. Ghostly. Despite being pretty pale, even in the height of summer, this camera somehow made me appear even more washed out. </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="P5xbEjiao3ZFPEqc6wgo6D" name="Dell Pro 7 vc macbook pro m2" alt="Dell Pro 7 (left), MacBook Pro M2 (right)" src="https://cdn.mos.cms.futurecdn.net/P5xbEjiao3ZFPEqc6wgo6D-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The image on the left is taken with the Dell Pro 7 Webcam, and the image on the right is taken using the built-in camera on my MacBook Pro M2. The MacBook image has a lot more life and color in my skin, and I think this makes me seem more personable on-screen and means I engage with the person on the other end of the call a lot better. </p><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-verdict"><span>Dell Pro 7 Webcam review: Verdict</span></h2><p>The Dell Pro 7 Webcam is a decent choice for at-home office workers — it has a lot to offer. The effortless setup means you can have it recording in an instant, and the auto-adjustment settings mean I didn’t have to mess around with my camera settings. </p><p>The microphone quality is great for ensuring my voice is clear on calls, and the privacy lens added a layer of security and meant I could easily turn my camera off if I needed to. </p><p>However, the camera and mount are very heavy, making it hard to balance on a computer screen. Also, I thought the color replication was poor in comparison to my usual webcam. At $199, I would expect a little more. You can get better for the price, such as the Insta360 Link 2C Pro, but it’s perfect for anyone who needs something reliable for everyday use. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/dell-pro-7-webcam-review</link>
                                                                            <description>
                            <![CDATA[ The Dell Pro 7 Webcam is a good choice for home office workers, but it’s so heavy you could also use it as a paperweight. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">R9AYVSX9Sm5M4FYzesHqXU</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/ZvenyUFaUDSZ8rVchsreH7-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 14 Aug 2026 15:10:09 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></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-320-70.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/ZvenyUFaUDSZ8rVchsreH7-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Dell Pro 7 Webcam photographed in front of a blue background]]></media:description>                                                            <media:text><![CDATA[Dell Pro 7 Webcam photographed in front of a blue background]]></media:text>
                                <media:title type="plain"><![CDATA[Dell Pro 7 Webcam photographed in front of a blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/ZvenyUFaUDSZ8rVchsreH7-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Depending on your home-office needs, the Dell Pro 7 Webcam could be the <a href="https://www.tomsguide.com/computing/peripherals/best-webcams">best webcam</a> to add to your video conferencing setup. The easy setup means you can have it up and running in seconds, and the auto-adjustments based on AI-powered auto adjustments help you stay looking professional. The microphone quality is good, and the privacy lens is helpful for quickly switching off your camera. </p><p>But it’s worth keeping in mind that the mount and the camera are heavier than most others on the market, and the color replication is poor. I also think you can get better for the price tag. </p><p>To find out more, you can keep reading this full Dell Pro 7 Webcam review.</p><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-cheat-sheet"><span>Dell Pro 7 Webcam review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 4K webcam with smart modes for auto-adjustment</li><li><strong>Who is it for?</strong> At-home office workers who want to upgrade their setup</li><li><strong>What does it cost?</strong> The Dell Pro 7 Webcam is available for <a href="https://www.dell.com/en-us/shop/dell-pro-7-webcam-4k-wb726/apd/319-bbmt/pc-accessories" target="_blank" rel="nofollow">$199 / £190</a> from the Dell online store</li><li><strong>What do we like?</strong> It’s easy to set up, automatically adjusts exposure and white balance, has good microphone quality and a privacy lens</li><li><strong>What don’t we like?</strong> The mount is very heavy and the color replication is poor</li></ul><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-specs"><span>Dell Pro 7 Webcam review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.dell.com/en-us/shop/dell-pro-7-webcam-4k-wb726/apd/319-bbmt/pc-accessories" target="_blank" rel="nofollow">$199</a> / <a href="https://www.dell.com/en-uk/shop/dell-pro-7-webcam-4k-wb726/apd/722-bbcv/pc-accessories" target="_blank" rel="nofollow">£190</a></p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>2.6 x 2.1 x 1.4 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>8.5 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Maximum monitor thickness support</strong></p></td><td  ><p>1.8 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired </p></td></tr><tr><td class="firstcol " ><p><strong>Maximum video resolution</strong></p></td><td  ><p>4K (3840 x 2160)</p></td></tr><tr><td class="firstcol " ><p><strong>Maximum frame rate </strong></p></td><td  ><p>60fps</p></td></tr><tr><td class="firstcol " ><p><strong>Digital zoom</strong></p></td><td  ><p>x4</p></td></tr><tr><td class="firstcol " ><p><strong>Supported video modes</strong></p></td><td  ><p>2160p, 1080p, 720p</p></td></tr><tr><td class="firstcol " ><p><strong>Image processing technology</strong></p></td><td  ><p>Advanced AI-Imaging with Smart Enhancement</p></td></tr><tr><td class="firstcol " ><p><strong>Maximum aperture</strong></p></td><td  ><p>f/2.0</p></td></tr><tr><td class="firstcol " ><p><strong>Microphone range</strong></p></td><td  ><p>Up to 10 feet</p></td></tr><tr><td class="firstcol " ><p><strong>Supported operating systems</strong></p></td><td  ><p>Windows 11, macOS 14 or higher</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-the-ups"><span>Dell Pro 7 Webcam review: The ups</span></h2><p>The Dell Pro 7 Webcam has a lot to like. With its effortless setup and intelligent features, it’s a breeze to integrate into your existing work routine. </p><h2 id="easy-to-set-up">Easy to set up</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Lp7rgyDUNBv4DJFDzRDTt8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/Lp7rgyDUNBv4DJFDzRDTt8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam with your computer couldn’t be easier (once you balance it, but more on that later). The camera comes with a 10-foot USB-C cable, plus a USB-A converter for compatibility with older devices. Simply plug this into your computer, and you’re done. </p><p>I had to switch from my built-in laptop webcam to the external one, but you won’t need to do this if you’re using a PC. I did the same for the microphone to make sure I was using the Dell camera’s mic. </p><h2 id="intelligent-features">Intelligent 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="ku2bP3gLWHLoeP3XirEYv8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/ku2bP3gLWHLoeP3XirEYv8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam offers a few intelligent features to make things a little easier. The camera uses Advanced AI-Imaging with Smart Enhancement, but this is just a fancy way of saying that it considers your face, environment, and movements to apply the best exposure settings to the camera. </p><p>It automatically adjusts exposure and white balance using Smart Scene Detection, which mostly worked well. However, other webcams, like the <a href="https://www.tomsguide.com/computing/peripherals/logitech-mx-brio-705-for-business-webcam-review">Logitech MX BRIO 750</a> for $199, perform better in low-light conditions. </p><p>If you want something a bit more high-tech for presenting, with impressive gimbal tracking and whiteboard focus settings, you should look at the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2-pro-review">Insta360 Link 2 Pro</a> ($249). It’s a little more expensive, but it opens up a whole new level of possibility when taking work calls and presenting ideas. </p><h2 id="good-microphone">Good microphone</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="eWmZvy53FPUNerBjV3fV58" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/eWmZvy53FPUNerBjV3fV58-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 on-board microphone of the Dell Pro 7 Webcam replicated my voice clearly. I sounded slightly crisper than when I use my <a href="https://www.tomsguide.com/reviews/apple-airpods-3">Apple AirPods 3</a>. It effectively reduced ambient sound around me and didn’t pick up any typing sounds from my keyboard. </p><h2 id="privacy-lens">Privacy lens</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dnuRa4psKvvMKiMCTvxLu8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/dnuRa4psKvvMKiMCTvxLu8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam has a built-in privacy lens that is controlled by a switch at the top. This quickly shuts off the camera view during meetings, so you don’t need to fiddle with settings on your computer.</p><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-the-downs"><span>Dell Pro 7 Webcam review: The downs</span></h2><p>While the Dell Pro 7 Webcam has a lot to love, there are a few things that put me off. The color replication in particular made my saturation-loving self shy away from this camera. </p><h2 id="heavy-mount">Heavy mount </h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NFZSmpAGNqyvUhd4FHq4N8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/NFZSmpAGNqyvUhd4FHq4N8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam mount is very heavy and is made even heavier by the camera. This meant that I found it difficult to balance, and it was actually pulling down my laptop screen. </p><p>The effect of this isn’t as bad if you’re mounting it to a larger monitor, but it still makes it tricky during the initial setup to get the right balance. The whole unit weighs 8.5 ounces. In comparison, the <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-pro-review">Insta360 Link 2C Pro</a> (with more capabilities) weighs just 4 ounces, while the <a href="https://www.tomsguide.com/computing/peripherals/opal-tadpole-webcam-review-desktop-users-must-keep-kissing-frogs">Opal Tadpole</a> weighs just 1.2 ounces but only works with laptops.</p><h2 id="color-replication">Color replication</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="58JvjVAPqLzHSfEGyVMxC8" name="Dell Pro 7 Webcam" alt="Dell Pro 7 Webcam photographed in front of a blue background" src="https://cdn.mos.cms.futurecdn.net/58JvjVAPqLzHSfEGyVMxC8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Dell Pro 7 Webcam made me look gaunt. Haunting. Ghostly. Despite being pretty pale, even in the height of summer, this camera somehow made me appear even more washed out. </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="P5xbEjiao3ZFPEqc6wgo6D" name="Dell Pro 7 vc macbook pro m2" alt="Dell Pro 7 (left), MacBook Pro M2 (right)" src="https://cdn.mos.cms.futurecdn.net/P5xbEjiao3ZFPEqc6wgo6D-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The image on the left is taken with the Dell Pro 7 Webcam, and the image on the right is taken using the built-in camera on my MacBook Pro M2. The MacBook image has a lot more life and color in my skin, and I think this makes me seem more personable on-screen and means I engage with the person on the other end of the call a lot better. </p><h2 class="article-body__section" id="section-dell-pro-7-webcam-review-verdict"><span>Dell Pro 7 Webcam review: Verdict</span></h2><p>The Dell Pro 7 Webcam is a decent choice for at-home office workers — it has a lot to offer. The effortless setup means you can have it recording in an instant, and the auto-adjustment settings mean I didn’t have to mess around with my camera settings. </p><p>The microphone quality is great for ensuring my voice is clear on calls, and the privacy lens added a layer of security and meant I could easily turn my camera off if I needed to. </p><p>However, the camera and mount are very heavy, making it hard to balance on a computer screen. Also, I thought the color replication was poor in comparison to my usual webcam. At $199, I would expect a little more. You can get better for the price, such as the Insta360 Link 2C Pro, but it’s perfect for anyone who needs something reliable for everyday use. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The Epomaker RT100 Pro strikes a great balance between whimsy and functionality — and it’s so worth the $119 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Epomaker has knocked it out of the park once again with the RT100 Pro, a full-size mechanical keyboard that gives you very little to complain about. Offering a beautiful typing experience thanks to its smooth linear switches, and featuring 1,000Hz polling for casual gaming, the RT100 Pro does it all. It comes with a customizable screen, too, which can be removed if you want four extra switches.</p><p>I’m also very happy to see Epomaker make competent web-based software that negates the need for downloading proprietary software. And while the screen doesn’t always work as intended, it’s hard to find any other glaring faults — the RT100 Pro is simply <em>that </em>good.</p><p>To find out if this is the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> for you, read my full Epomaker RT100 Pro review.</p><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-specs"><span>Epomaker RT100 Pro 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/EPOMAKER-RT100-PRO-Wireless-Keyboard/dp/B0GL8G7ZXS/" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/EPOMAKER-RT100-PRO-Tri-Mode-Mechanical/dp/B0GL896CZG/" target="_blank" rel="nofollow">£129</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Epomaker Creamy Jade (linear) and Sea Salt Silent V2 (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic + aluminum alloy</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>100%, 103 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz), 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>5,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>17.05 x 5.59 x 1.77 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.86lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Retro White, Pink</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-cheat-sheet"><span>Epomaker RT100 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A full-size mechanical keyboard with linear switches and a swappable screen</li><li><strong>Who is it for? </strong>Hardcore typists who are also casual gamers</li><li><strong>How much does it cost? </strong>The Epomaker RT100 Pro is available for <a href="https://www.amazon.com/EPOMAKER-RT100-PRO-Wireless-Keyboard/dp/B0GL8G7ZXS/" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/EPOMAKER-RT100-PRO-Tri-Mode-Mechanical/dp/B0GL896CZG/" target="_blank" rel="nofollow">£129</a></li><li><strong>What do we like? </strong>The stunning typing experience and sound profile, customizable screen, user-friendly web-based software, and more</li><li><strong>What don’t we like? </strong>The screen can sometimes be unreliable, and there are no stock tactile switches for sale</li></ul><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-the-ups"><span>Epomaker RT100 Pro review: The ups</span></h2><p>From its solid build and great design, to its wonderful switches and user-friendly software, the Epomaker RT100 Pro is a superb mechanical keyboard.</p><h2 id="improvements-galore">Improvements galore!</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/Lj9e8KU6tzjmJppe6mqQRB-1920-80.jpg" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" /><figcaption>Epomaker RT100 Pro.<small role="credit">Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/BTVYEjiP2cKQeYbYw4NH4g-1920-80.jpg" alt="A beige, brown and off-white Epomaker RT100 wireless mechanical keyboard" /><figcaption>Epomaker RT100.<small role="credit">Tom's Guide</small></figcaption></figure></figure><p>The Epomaker RT100 Pro improves on the original RT100 by introducing an aluminum alloy top plate that gives it more stability. While the RT100 featured an all-plastic chassis that made it feel slightly cheap, the RT100 Pro feels more premium and like it could stand the test of time. It still packs gasket PCB mounting that offers a soft typing feel and reduces vibrations while typing (more on that shortly).</p><p>Epomaker has switched up the keyboard’s layout, too. The RT100 Pro is a 100% full-size keyboard, compared to the 95% RT100. Alongside better-placed page navigation keys, you get a detachable, customizable screen. The brand has also fixed one of my biggest issues with the RT100: that pesky, toy-like, all-too-plasticky volume knob is gone and has now been replaced by a more premium dial.</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="oXpV2JZ6z3RGBRW7houcAB" name="Epomaker_RT100_pro 8.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/oXpV2JZ6z3RGBRW7houcAB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 improvement? The RT100 Pro’s 2.4GHz dongle is now stored in a separate, dedicated slot located on the keyboard’s underside. On the OG RT100, you had to remove the screen to slot its dongle into the onboard compartment. Basically, if you were using the screen, you risked losing the dongle, but not anymore.</p><p>Given its 100% layout, the RT100 Pro measures 17.05 x 5.59 x 1.77 inches and weighs 2.86lbs, so it’s best left stationery on your desk. If you want a solid mechanical keyboard to travel with, I’d recommend the low-profile<a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review"> IQUNIX MQ80</a> ($169), the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139) or the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-luma84-review">Epomaker Luma84</a> ($109).</p><h2 id="a-dream-to-type-on-and-listen-to">A dream to type on (and listen to)</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7w2TdknEH3Da7t6deVcRbB" name="Epomaker_RT100_pro 2.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/7w2TdknEH3Da7t6deVcRbB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 plethora of Epomaker keyboards, and I’m yet to be disappointed by the typing experience they offer. The Epomaker RT100 Pro is no different. As I mentioned earlier, the RT100 Pro features a gasket-mount design that ensures a soft type feel, and it negates any unnecessary metallic pings that would otherwise disrupt your typing experience.</p><p>Combined with the double-shot PBT keycaps and several layers of sound dampening foams, the RT100 Pro sounds creamy and thocky. It isn’t too loud that it disturbs your coworkers, and it isn’t too silent that you can’t appreciate the wonderful sound profile.</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="7ffDL8bVEDyzrPfKUwAZLB" name="Epomaker_RT100_pro 9.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/7ffDL8bVEDyzrPfKUwAZLB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, of course, depends on the switches you’re using, and there are two stock sets to choose from: Creamy Jade and Sea Salt V2. Both are linear switches, but the latter are much quieter — I tested the V1 version with the RT100 and they are indeed <em>very </em>quiet.</p><p>With the RT100 Pro, I tested the Creamy Jade switches which are lovely to type on. These require 45g of force to actuate, so they’re slightly heavier than, say, Wisteria V2s you find in the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139). Even so, they aren’t fatiguing in the slightest — I wrote this full review using the RT100 Pro. They also have a short pre-travel distance of 2mm so you don’t have to press them all the way down to activate them.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Epomaker RT100 Pro</p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-kw-300-mx-review">Cherry KW 300 MX</a></p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.78</p></td><td  ><p>91.25%</p></td></tr></tbody></table></div><p>I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> as soon as I unboxed the RT100 Pro, and as you can see in the table above, I was able to achieve 96 words per minute with 94.1% accuracy straight out of the box. The Glyph is still the fastest keyboard I’ve typed on, but I’m pleased with the RT100 Pro’s performance, too.</p><h2 id="it-ll-game-too">It’ll game, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="i3ZjLek8AfQmAjMLgREFaB" name="Epomaker_RT100_pro 5.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/i3ZjLek8AfQmAjMLgREFaB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though it’s a productivity-first keyboard, the Epomaker RT100 Pro is a good keyboard for gaming between tasks, too. Over wired and 2.4GHz connections, the RT100 Pro polls at 1,000Hz — this naturally drops to 125Hz over Bluetooth. The 1,000Hz polling rate combined with the linear switches makes the RT100 Pro decent for racing, FPS and RPGs.</p><p>I tested the keyboard by playing some Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/forza-horizon-5">Forza Horizon 5</a> on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a>. I found the keyboard to be extremely responsive. Each command was registered instantaneously, and there was no input lag or latency issues, thanks to 1KHz polling. The Creamy Jade switches themselves felt smooth, and because of the short pre-travel distance, I appreciated being able to quickly execute commands without my fingers feeling tired.</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="gcuiQYkupriyMswuSF7bYB" name="Epomaker_RT100_pro 7.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gcuiQYkupriyMswuSF7bYB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 RT100 Pro also features N-key rollover, which enables multiple inputs to be registered at the same time. This came in handy in Counter-Strike 2 where I was able to crouch and open my inventory at the same time, therefore saving me a couple of seconds and giving me a slight competitive advantage. In Forza Horizon 5, my tight hairpin turns didn’t suffer from missed commands as I was able to quickly transition between braking, handbraking and steering my car.</p><p>If you’re a serious or competitive gamer, I’d always recommend picking up a Hall Effect keyboard with adjustable actuation, such as the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65</a> ($99) or the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k4-he-keyboard-review">Keychron K4 HE</a> ($134). If you’re someone who cares about headline specs, like 8,000Hz polling (which is usually overkill for most players), you can always grab the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a> ($74) mechanical deck instead.</p><h2 id="customizable-detachable-screen">Customizable, detachable screen</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6114px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2t72hB5373DvJGbDsjTddh" name="Epo_RT100_Pro-3" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/2t72hB5373DvJGbDsjTddh-1920-80.jpg" mos="" align="middle" fullscreen="" width="6114" height="3439" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Screens on keyboards are becoming increasingly common. I’ve seen them on the Epomaker Glyph, the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> ($259), the <a href="https://www.tomsguide.com/gaming/peripherals/steelseries-apex-pro-review">SteelSeries Apex Pro </a>($199), and now on the Epomaker RT100 Pro. Located above the page navigation keys, the RT100 Pro features a 1.54-inch square TFT screen that, by default, displays the keyboard’s vitals, like battery life, active operating system layout, and more.</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:596px;"><p class="vanilla-image-block" style="padding-top:56.04%;"><img id="2bZuYn6n45EFzDLhwtAMvg" name="Adobe Express - PXL_20260813_094936409~2" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/2bZuYn6n45EFzDLhwtAMvg-1920-80.gif" mos="" align="middle" fullscreen="" width="596" height="334" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Marvel Studios / Disney+ / Tom's Guide)</span></figcaption></figure><p>Through web-based software (more on that soon), you can upload GIFs, static images or pixel art to the screen. The TFT display itself is bright and colorful, and what’s more, you can actually remove the screen to reveal four additional switch sockets. Included in the RT100 Pro’s box are extra switches which can be popped into the four sockets and then their functions can be customized via Epomaker Hub. That’s handy for anyone who wants extra functions — but I personally kept the screen attached, displaying a GIF of Kathryn Hahn from <a href="https://www.tomsguide.com/news/wandavision-review-this-is-the-marvel-show-we-needed">WandaVision</a> on loop.</p><h2 id="user-friendly-web-based-software">User-friendly web-based software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/xFMxT9q68ZYPpF8DhBWe33-1920-80.jpg" alt="Epomaker Hub screenshot" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/mivxgrGqhDhFNjsFiUTiq-1920-80.jpg" alt="Epomaker Hub screenshot" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/8cvfcsUkF5dGP55qWj7Ms-1920-80.jpg" alt="Epomaker Hub screenshot" /><figcaption><small role="credit">Epomaker</small></figcaption></figure></figure><p>While the Epomaker RT100 was customizable via proprietary, downloadable software, the Epomaker RT100 Pro is compatible with the brand’s new web-based <a href="https://hub.epomaker.com/" target="_blank" rel="nofollow">Epomaker Hub</a> — and it’s a hoot to use. It looks similar to Keychron Launcher, but if you aren’t familiar with either, it won’t take you time getting to grips with Epomaker Hub.</p><p>The web-based app sports a clean, user-friendly interface, and there are no intrusive ads or pop-ups asking you to sign up every time you switch tabs (looking at you, Asus Armoury Crate). You can use Epomaker Hub to remap the RT100 Pro’s keys, record macros, adjust the per-key RGB lighting or choose a different effect, and upload GIFs or static images to the screen. Everything is easy to understand and do.</p><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-the-downs"><span>Epomaker RT100 Pro review: The downs</span></h2><p>There isn’t a lot wrong with the Epomaker RT100 Pro, but it’s important to note that its detachable screen doesn’t always work as intended.</p><h2 id="the-screen-can-be-finicky">The screen can be finicky</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6070px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="gGynNnN4SHTbiVn74z5t8h" name="Epo_RT100_Pro" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gGynNnN4SHTbiVn74z5t8h-1920-80.jpg" mos="" align="middle" fullscreen="" width="6070" height="3414" attribution="" endorsement="" class="inline"></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 think it goes without saying that I love the Epomaker RT100 Pro’s customizable and detachable screen — but it isn’t perfect. In my testing, the screen malfunctioned every time I removed and put it back in. Sometimes, it wouldn’t display the correct time (even though it did before being removed). Other times, it would go blank, and all I could see was a dark blue screen.</p><p>There was also an instance where the screen just wouldn’t turn on — I had to remove and insert it a few times to get it working again. All of this is quite disappointing, given that the idea behind the screen is fantastic. I just wish it worked better and was more reliable. Even though the OG RT100’s TV-like screen felt a little gimmicky, it never actually stopped working in my testing.</p><h2 id="no-stock-tactile-switches">No stock tactile switches</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PdMKkwQ8E7qcLdz59H4WXB" name="Epomaker_RT100_pro 4.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/PdMKkwQ8E7qcLdz59H4WXB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mentioned earlier, the Epomaker RT100 Pro comes with either Creamy Jade or Sea Salt V2 stock switches — both of which are linear. Unfortunately, there aren’t any stock tactile switches available. The keyboard is hot-swappable and compatible with 3- and 5-pin south-facing MX switches, so you can simply pop in your own without de- and re-soldering them to the PCB.</p><p>That will be an added expenditure, though, but not a massive one. A 108-piece set of <a href="https://www.amazon.com/Zjmehty-Mechanical-Keyboard-Switches-Waterproof/dp/B0CBPQGBSF/" target="_blank" rel="nofollow">Kailh Box White 45g switches costs $32</a>, or you can get 108 pieces of <a href="https://www.amazon.com/Zjmehty-Switches-Mechanical-Pre-Lubed-Pin-Enhanced/dp/B0CF8DWDGF/" target="_blank" rel="nofollow">Gateron G Pro 3.0 Brown 55g switches for $29</a>.</p><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-verdict"><span>Epomaker RT100 Pro 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="MuD4XMHPzVRRiT9MtdWVjB" name="Epomaker_RT100_pro 6.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/MuD4XMHPzVRRiT9MtdWVjB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Epomaker RT100 Pro introduces small but meaningful improvements to the RT100, making it one of the finest mechanical keyboards you can buy today. Better building materials that make it feel premium, a detachable screen that can be removed for four extra switches, and user-friendly web-based software are most welcome.</p><p>If you’re a hardcore typist, like me, the RT100 Pro won’t let you down. Its pre-lubed linear switches are delightful to type on, and they’re lovely to listen to, too. And thanks to 1,000Hz polling, the RT100 Pro is a solid keyboard for casual gamers. While the screen doesn’t always work as intended, it’s a small flaw overshadowed by everything else on offer.</p><p>Anyone looking for a competent full-size mechanical keyboard and with $119 to spare should consider the RT100 Pro as their daily driver. It puts the ‘fun’ in ‘functional.’</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/epomaker-rt100-pro-review</link>
                                                                            <description>
                            <![CDATA[ The Epomaker RT100 Pro is a lovely mechanical keyboard for typing, and it features a detachable screen that can be customized via web-based software. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">gPneajt9MoWjc5KiTzJ7mY</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/NArgk6guEyrWAmDYL2rooB-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 13 Aug 2026 14:03:45 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/NArgk6guEyrWAmDYL2rooB-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A white Epomaker RT100 Pro wireless mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A white Epomaker RT100 Pro wireless mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A white Epomaker RT100 Pro wireless mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/NArgk6guEyrWAmDYL2rooB-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Epomaker has knocked it out of the park once again with the RT100 Pro, a full-size mechanical keyboard that gives you very little to complain about. Offering a beautiful typing experience thanks to its smooth linear switches, and featuring 1,000Hz polling for casual gaming, the RT100 Pro does it all. It comes with a customizable screen, too, which can be removed if you want four extra switches.</p><p>I’m also very happy to see Epomaker make competent web-based software that negates the need for downloading proprietary software. And while the screen doesn’t always work as intended, it’s hard to find any other glaring faults — the RT100 Pro is simply <em>that </em>good.</p><p>To find out if this is the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> for you, read my full Epomaker RT100 Pro review.</p><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-specs"><span>Epomaker RT100 Pro 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/EPOMAKER-RT100-PRO-Wireless-Keyboard/dp/B0GL8G7ZXS/" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/EPOMAKER-RT100-PRO-Tri-Mode-Mechanical/dp/B0GL896CZG/" target="_blank" rel="nofollow">£129</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Epomaker Creamy Jade (linear) and Sea Salt Silent V2 (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic + aluminum alloy</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>100%, 103 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz), 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>5,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>17.05 x 5.59 x 1.77 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.86lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Retro White, Pink</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-cheat-sheet"><span>Epomaker RT100 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A full-size mechanical keyboard with linear switches and a swappable screen</li><li><strong>Who is it for? </strong>Hardcore typists who are also casual gamers</li><li><strong>How much does it cost? </strong>The Epomaker RT100 Pro is available for <a href="https://www.amazon.com/EPOMAKER-RT100-PRO-Wireless-Keyboard/dp/B0GL8G7ZXS/" target="_blank" rel="nofollow">$119</a> / <a href="https://www.amazon.co.uk/EPOMAKER-RT100-PRO-Tri-Mode-Mechanical/dp/B0GL896CZG/" target="_blank" rel="nofollow">£129</a></li><li><strong>What do we like? </strong>The stunning typing experience and sound profile, customizable screen, user-friendly web-based software, and more</li><li><strong>What don’t we like? </strong>The screen can sometimes be unreliable, and there are no stock tactile switches for sale</li></ul><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-the-ups"><span>Epomaker RT100 Pro review: The ups</span></h2><p>From its solid build and great design, to its wonderful switches and user-friendly software, the Epomaker RT100 Pro is a superb mechanical keyboard.</p><h2 id="improvements-galore">Improvements galore!</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/Lj9e8KU6tzjmJppe6mqQRB-1920-80.jpg" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" /><figcaption>Epomaker RT100 Pro.<small role="credit">Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/BTVYEjiP2cKQeYbYw4NH4g-1920-80.jpg" alt="A beige, brown and off-white Epomaker RT100 wireless mechanical keyboard" /><figcaption>Epomaker RT100.<small role="credit">Tom's Guide</small></figcaption></figure></figure><p>The Epomaker RT100 Pro improves on the original RT100 by introducing an aluminum alloy top plate that gives it more stability. While the RT100 featured an all-plastic chassis that made it feel slightly cheap, the RT100 Pro feels more premium and like it could stand the test of time. It still packs gasket PCB mounting that offers a soft typing feel and reduces vibrations while typing (more on that shortly).</p><p>Epomaker has switched up the keyboard’s layout, too. The RT100 Pro is a 100% full-size keyboard, compared to the 95% RT100. Alongside better-placed page navigation keys, you get a detachable, customizable screen. The brand has also fixed one of my biggest issues with the RT100: that pesky, toy-like, all-too-plasticky volume knob is gone and has now been replaced by a more premium dial.</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="oXpV2JZ6z3RGBRW7houcAB" name="Epomaker_RT100_pro 8.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/oXpV2JZ6z3RGBRW7houcAB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 improvement? The RT100 Pro’s 2.4GHz dongle is now stored in a separate, dedicated slot located on the keyboard’s underside. On the OG RT100, you had to remove the screen to slot its dongle into the onboard compartment. Basically, if you were using the screen, you risked losing the dongle, but not anymore.</p><p>Given its 100% layout, the RT100 Pro measures 17.05 x 5.59 x 1.77 inches and weighs 2.86lbs, so it’s best left stationery on your desk. If you want a solid mechanical keyboard to travel with, I’d recommend the low-profile<a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review"> IQUNIX MQ80</a> ($169), the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139) or the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-luma84-review">Epomaker Luma84</a> ($109).</p><h2 id="a-dream-to-type-on-and-listen-to">A dream to type on (and listen to)</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7w2TdknEH3Da7t6deVcRbB" name="Epomaker_RT100_pro 2.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/7w2TdknEH3Da7t6deVcRbB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 plethora of Epomaker keyboards, and I’m yet to be disappointed by the typing experience they offer. The Epomaker RT100 Pro is no different. As I mentioned earlier, the RT100 Pro features a gasket-mount design that ensures a soft type feel, and it negates any unnecessary metallic pings that would otherwise disrupt your typing experience.</p><p>Combined with the double-shot PBT keycaps and several layers of sound dampening foams, the RT100 Pro sounds creamy and thocky. It isn’t too loud that it disturbs your coworkers, and it isn’t too silent that you can’t appreciate the wonderful sound profile.</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="7ffDL8bVEDyzrPfKUwAZLB" name="Epomaker_RT100_pro 9.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/7ffDL8bVEDyzrPfKUwAZLB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, of course, depends on the switches you’re using, and there are two stock sets to choose from: Creamy Jade and Sea Salt V2. Both are linear switches, but the latter are much quieter — I tested the V1 version with the RT100 and they are indeed <em>very </em>quiet.</p><p>With the RT100 Pro, I tested the Creamy Jade switches which are lovely to type on. These require 45g of force to actuate, so they’re slightly heavier than, say, Wisteria V2s you find in the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> ($139). Even so, they aren’t fatiguing in the slightest — I wrote this full review using the RT100 Pro. They also have a short pre-travel distance of 2mm so you don’t have to press them all the way down to activate them.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Epomaker RT100 Pro</p></td><td  ><p>96</p></td><td  ><p>94.1%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review">IQUNIX MQ80</a></p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-kw-300-mx-review">Cherry KW 300 MX</a></p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.78</p></td><td  ><p>91.25%</p></td></tr></tbody></table></div><p>I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> as soon as I unboxed the RT100 Pro, and as you can see in the table above, I was able to achieve 96 words per minute with 94.1% accuracy straight out of the box. The Glyph is still the fastest keyboard I’ve typed on, but I’m pleased with the RT100 Pro’s performance, too.</p><h2 id="it-ll-game-too">It’ll game, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="i3ZjLek8AfQmAjMLgREFaB" name="Epomaker_RT100_pro 5.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/i3ZjLek8AfQmAjMLgREFaB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though it’s a productivity-first keyboard, the Epomaker RT100 Pro is a good keyboard for gaming between tasks, too. Over wired and 2.4GHz connections, the RT100 Pro polls at 1,000Hz — this naturally drops to 125Hz over Bluetooth. The 1,000Hz polling rate combined with the linear switches makes the RT100 Pro decent for racing, FPS and RPGs.</p><p>I tested the keyboard by playing some Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/forza-horizon-5">Forza Horizon 5</a> on the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Alienware 16X Aurora</a>. I found the keyboard to be extremely responsive. Each command was registered instantaneously, and there was no input lag or latency issues, thanks to 1KHz polling. The Creamy Jade switches themselves felt smooth, and because of the short pre-travel distance, I appreciated being able to quickly execute commands without my fingers feeling tired.</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="gcuiQYkupriyMswuSF7bYB" name="Epomaker_RT100_pro 7.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gcuiQYkupriyMswuSF7bYB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 RT100 Pro also features N-key rollover, which enables multiple inputs to be registered at the same time. This came in handy in Counter-Strike 2 where I was able to crouch and open my inventory at the same time, therefore saving me a couple of seconds and giving me a slight competitive advantage. In Forza Horizon 5, my tight hairpin turns didn’t suffer from missed commands as I was able to quickly transition between braking, handbraking and steering my car.</p><p>If you’re a serious or competitive gamer, I’d always recommend picking up a Hall Effect keyboard with adjustable actuation, such as the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65</a> ($99) or the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k4-he-keyboard-review">Keychron K4 HE</a> ($134). If you’re someone who cares about headline specs, like 8,000Hz polling (which is usually overkill for most players), you can always grab the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a> ($74) mechanical deck instead.</p><h2 id="customizable-detachable-screen">Customizable, detachable screen</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6114px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2t72hB5373DvJGbDsjTddh" name="Epo_RT100_Pro-3" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/2t72hB5373DvJGbDsjTddh-1920-80.jpg" mos="" align="middle" fullscreen="" width="6114" height="3439" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Screens on keyboards are becoming increasingly common. I’ve seen them on the Epomaker Glyph, the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> ($259), the <a href="https://www.tomsguide.com/gaming/peripherals/steelseries-apex-pro-review">SteelSeries Apex Pro </a>($199), and now on the Epomaker RT100 Pro. Located above the page navigation keys, the RT100 Pro features a 1.54-inch square TFT screen that, by default, displays the keyboard’s vitals, like battery life, active operating system layout, and more.</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:596px;"><p class="vanilla-image-block" style="padding-top:56.04%;"><img id="2bZuYn6n45EFzDLhwtAMvg" name="Adobe Express - PXL_20260813_094936409~2" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/2bZuYn6n45EFzDLhwtAMvg-1920-80.gif" mos="" align="middle" fullscreen="" width="596" height="334" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Marvel Studios / Disney+ / Tom's Guide)</span></figcaption></figure><p>Through web-based software (more on that soon), you can upload GIFs, static images or pixel art to the screen. The TFT display itself is bright and colorful, and what’s more, you can actually remove the screen to reveal four additional switch sockets. Included in the RT100 Pro’s box are extra switches which can be popped into the four sockets and then their functions can be customized via Epomaker Hub. That’s handy for anyone who wants extra functions — but I personally kept the screen attached, displaying a GIF of Kathryn Hahn from <a href="https://www.tomsguide.com/news/wandavision-review-this-is-the-marvel-show-we-needed">WandaVision</a> on loop.</p><h2 id="user-friendly-web-based-software">User-friendly web-based software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/xFMxT9q68ZYPpF8DhBWe33-1920-80.jpg" alt="Epomaker Hub screenshot" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/mivxgrGqhDhFNjsFiUTiq-1920-80.jpg" alt="Epomaker Hub screenshot" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/8cvfcsUkF5dGP55qWj7Ms-1920-80.jpg" alt="Epomaker Hub screenshot" /><figcaption><small role="credit">Epomaker</small></figcaption></figure></figure><p>While the Epomaker RT100 was customizable via proprietary, downloadable software, the Epomaker RT100 Pro is compatible with the brand’s new web-based <a href="https://hub.epomaker.com/" target="_blank" rel="nofollow">Epomaker Hub</a> — and it’s a hoot to use. It looks similar to Keychron Launcher, but if you aren’t familiar with either, it won’t take you time getting to grips with Epomaker Hub.</p><p>The web-based app sports a clean, user-friendly interface, and there are no intrusive ads or pop-ups asking you to sign up every time you switch tabs (looking at you, Asus Armoury Crate). You can use Epomaker Hub to remap the RT100 Pro’s keys, record macros, adjust the per-key RGB lighting or choose a different effect, and upload GIFs or static images to the screen. Everything is easy to understand and do.</p><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-the-downs"><span>Epomaker RT100 Pro review: The downs</span></h2><p>There isn’t a lot wrong with the Epomaker RT100 Pro, but it’s important to note that its detachable screen doesn’t always work as intended.</p><h2 id="the-screen-can-be-finicky">The screen can be finicky</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:6070px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="gGynNnN4SHTbiVn74z5t8h" name="Epo_RT100_Pro" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gGynNnN4SHTbiVn74z5t8h-1920-80.jpg" mos="" align="middle" fullscreen="" width="6070" height="3414" attribution="" endorsement="" class="inline"></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 think it goes without saying that I love the Epomaker RT100 Pro’s customizable and detachable screen — but it isn’t perfect. In my testing, the screen malfunctioned every time I removed and put it back in. Sometimes, it wouldn’t display the correct time (even though it did before being removed). Other times, it would go blank, and all I could see was a dark blue screen.</p><p>There was also an instance where the screen just wouldn’t turn on — I had to remove and insert it a few times to get it working again. All of this is quite disappointing, given that the idea behind the screen is fantastic. I just wish it worked better and was more reliable. Even though the OG RT100’s TV-like screen felt a little gimmicky, it never actually stopped working in my testing.</p><h2 id="no-stock-tactile-switches">No stock tactile switches</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PdMKkwQ8E7qcLdz59H4WXB" name="Epomaker_RT100_pro 4.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/PdMKkwQ8E7qcLdz59H4WXB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mentioned earlier, the Epomaker RT100 Pro comes with either Creamy Jade or Sea Salt V2 stock switches — both of which are linear. Unfortunately, there aren’t any stock tactile switches available. The keyboard is hot-swappable and compatible with 3- and 5-pin south-facing MX switches, so you can simply pop in your own without de- and re-soldering them to the PCB.</p><p>That will be an added expenditure, though, but not a massive one. A 108-piece set of <a href="https://www.amazon.com/Zjmehty-Mechanical-Keyboard-Switches-Waterproof/dp/B0CBPQGBSF/" target="_blank" rel="nofollow">Kailh Box White 45g switches costs $32</a>, or you can get 108 pieces of <a href="https://www.amazon.com/Zjmehty-Switches-Mechanical-Pre-Lubed-Pin-Enhanced/dp/B0CF8DWDGF/" target="_blank" rel="nofollow">Gateron G Pro 3.0 Brown 55g switches for $29</a>.</p><h2 class="article-body__section" id="section-epomaker-rt100-pro-review-verdict"><span>Epomaker RT100 Pro 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="MuD4XMHPzVRRiT9MtdWVjB" name="Epomaker_RT100_pro 6.JPG" alt="A white Epomaker RT100 Pro wireless mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/MuD4XMHPzVRRiT9MtdWVjB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Epomaker RT100 Pro introduces small but meaningful improvements to the RT100, making it one of the finest mechanical keyboards you can buy today. Better building materials that make it feel premium, a detachable screen that can be removed for four extra switches, and user-friendly web-based software are most welcome.</p><p>If you’re a hardcore typist, like me, the RT100 Pro won’t let you down. Its pre-lubed linear switches are delightful to type on, and they’re lovely to listen to, too. And thanks to 1,000Hz polling, the RT100 Pro is a solid keyboard for casual gamers. While the screen doesn’t always work as intended, it’s a small flaw overshadowed by everything else on offer.</p><p>Anyone looking for a competent full-size mechanical keyboard and with $119 to spare should consider the RT100 Pro as their daily driver. It puts the ‘fun’ in ‘functional.’</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I added three extra keys to my MacBook with this unique accessory — and it's a game changer for my workflow ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Remember when devices were trying to get rid of buttons, and now more and more companies are working to add AI-centric ones? Step forward, Aina, a company offering something a little unique.</p><p>Its new keypad, Dune, is essentially a small macro pad you can throw in a bag to take on the go with your MacBook. And while I wasn’t sure I needed a context-specific series of buttons attached to my Mac, I couldn’t believe how intuitive it was.</p><div class="product"><a data-dimension112="58063e7a-909c-11f1-a388-2fdc8041c3c9" data-action="Deal Block" data-label="Dune Keypad" data-dimension48="Dune Keypad" data-dimension25="$149" href="https://www.aina.com/waitlist" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1079px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="zgWMsbAydycexBW6cwgCm8" name="Dune Keypad" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/zgWMsbAydycexBW6cwgCm8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1079" height="1079" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Dune keypad attaches directly to your MacBook’s USB-C port to add three fully customizable, context-aware physical buttons to your setup. It automatically swaps hotkey functions depending on your active app, making it easy to mute video calls, trigger Claude AI prompts, or trigger custom macros on the go. If you want Stream Deck-style control without lugging a desktop dock around, it is a smart, portable addition to a mobile workstation.<a class="view-deal button" href="https://www.aina.com/waitlist" target="_blank" rel="nofollow" data-dimension112="58063e7a-909c-11f1-a388-2fdc8041c3c9" data-action="Deal Block" data-label="Dune Keypad" data-dimension48="Dune Keypad" data-dimension25="$149">View Deal</a></p></div><h2 id="get-work-dune">Get work Dune</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:703px;"><p class="vanilla-image-block" style="padding-top:56.19%;"><img id="XxedE3YSqGodmmnTD3AzxM" name="dune keyboard" alt="Dune keyboard" src="https://cdn.mos.cms.futurecdn.net/XxedE3YSqGodmmnTD3AzxM-1920-80.png" mos="" align="middle" fullscreen="" width="703" height="395" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Dune is a small three-key keypad that plugs into your MacBook’s USB-C port on the left-hand side, with three colored, metal buttons, a solid shell that feels built to last, and a very welcome rubberized base to avoid scratching desks and tables.</p><p>Above the buttons is a window showing an LED inside, and my only gripe is that being able to look inside and see some of the circuitry feels like it goes against the rest of the design inspiration here - everything is so clean and fine-tuned that it feels as though the light that goes on when you press a button was a little bit of an afterthought.</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:722px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="X9KUiWyEaD6YtB2UyGxuhh" name="Screenshot 2026-07-31 155517" alt="dune keyboard" src="https://cdn.mos.cms.futurecdn.net/X9KUiWyEaD6YtB2UyGxuhh-1920-80.png" mos="" align="middle" fullscreen="" width="722" height="406" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Still, that’s genuinely my only nitpick here with the design. I have the Midnight MacBook Air (still rocking an M3 chip), and the paint chips like nobody’s business. As a result, I was worried about having an additional metal chassis potentially locked alongside it, but the Dune sits beautifully, with no wiggle from the USB-C port.</p><h2 id="so-what-does-dune-do">So what does Dune do?</h2><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:50.80%;"><img id="7AKC3wwA2kQWyQffoz72f3" name="Dune software 2" alt="Dune keypad" src="https://cdn.mos.cms.futurecdn.net/7AKC3wwA2kQWyQffoz72f3-1920-80.png" mos="" align="middle" fullscreen="" width="748" height="380" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Aina)</span></figcaption></figure><p>Here’s the fun part: Dune basically does anything you need it to. During the setup process, you can flag some key apps you use day-to-day. For my sins, mine include Teams, and Dune instantly offered some useful shortcuts attached to the hardware buttons, all displayed in the bottom-left corner.</p><p>Muting my mic, turning off the camera with a single touch, or simply bringing the meeting to the front of my active windows are all things I have to do regularly, so having the option to hit a clearly defined button for that is ideal.</p><p>Hitting a button to bring up a fresh Claude chat and using the other two buttons to instantly “yay” or “nay” its responses speeds things up by a few seconds each day, which soon adds up across a week, a month, or a year.</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="mdBdzH6SD8SdAT763EYfC3" name="8 - 2026-07-31T155700.784" alt="dune keyboard" src="https://cdn.mos.cms.futurecdn.net/mdBdzH6SD8SdAT763EYfC3-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Dune also cycles its context-specific commands depending on your active window, so pressing the buttons becomes muscle memory.</p><p>At present, the in-app marketplace is still being populated, but there are some solid options already. Being able to open your Mac’s clipboard, or instantly send a screenshot to Claude or update a Slack status are very useful, and I’m excited to see how things pan out from the perspective of power users getting their hands on Dune.</p><p>Sticking with Claude, however, it’s clear this is where Dune’s real appeal lies. In fact, you can build your own workflows for Dune using Claude, be those scripts, app-specific contextual actions, and more. Will you get the same joy from another AI app? It’s hard to say, but if you already have Claude, this is a genuinely intuitive way to build a better Mac.</p><h2 id="a-curio-or-a-must-have">A curio, or a must-have?</h2><p>There are some considerations, of course. For one, if your Mac is docked while you work, then a more traditional macro pad is likely to be more useful. For another, you’ll need a USB-C port free to be able to use Dune.</p><p>And yet, I can see myself packing it for every trip where I’m just taking the laptop and don’t need to plug into a variety of other peripherals.</p><p>For something that must’ve required plenty of thought, it somehow feels so simple to use. Muting meetings, triggering agentic AI, or just having a place to run a shortcut without trying to remember the keyboard combo I set up years ago is genuinely useful.</p><p>Now that I’ve used Dune, I’m very keen to see where it goes from here and how it evolves. As someone who uses a Stream Deck but doesn’t take it on the go, I’m happy to have an alternative that’s much more portable, and that leans nicely into agentic AI.</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/ai/claude/i-just-learned-your-claude-ai-chats-could-show-up-in-google-heres-how-to-check-yours"><strong>I just learned your Claude AI chats could show up in Google — here's how to check yours</strong></a></li><li><a href="https://www.tomsguide.com/ai/5-claude-prompts-to-replace-chatgpt-for-writing-and-analysis"><strong>I stopped using ChatGPT for everything — here are 5 prompts I only run in Claude</strong></a></li><li><a href="https://www.tomsguide.com/ai/i-use-gemini-spark-daily-here-are-7-prompts-that-show-what-it-can-really-do"><strong>I use Gemini Spark daily — here are 7 prompts that show what it can really do</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/ai/does-your-mac-need-new-buttons-great-news-the-dune-keypad-offers-just-that</link>
                                                                            <description>
                            <![CDATA[ Dune, is essentially a small macro pad you can throw in a bag to take on the go with your MacBook. Here's how it works. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">vuE4Hc7dU2vWF3CDHmF6ae</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/crWdvXZLzJBB5YpmUezRua-1920-80.png" type="image/png" length="0"></enclosure>
                                                                        <pubDate>Mon, 03 Aug 2026 11:30:00 +0000</pubDate>                                                                                                                                <updated>Wed, 05 Aug 2026 07:07:39 +0000</updated>
                                                                                                                                            <category><![CDATA[AI]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                                    <dc:creator><![CDATA[ Lloyd Coombes ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/xcWocVTwa9yiwXRs559XNA-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Lloyd Coombes is a freelance tech and fitness writer. He&#039;s an expert in all things Apple as well as in computer and gaming tech, with previous works published on TechRadar, Tom&#039;s Guide, Live Science and more. You&#039;ll find him regularly testing the latest MacBook or iPhone, but he spends most of his time writing about video games as Gaming Editor for the Daily Star. He also covers board games and virtual reality, just to round out the nerdy pursuits.&lt;/p&gt; ]]></dc:description>
                                                                                                                                <cf:isSponsored>false</cf:isSponsored>
                <cf:hasAffiliateLinks>false</cf:hasAffiliateLinks>
                <cf:isPaid>false</cf:isPaid>
                                                                                                                                <media:content type="image/png" url="https://cdn.mos.cms.futurecdn.net/crWdvXZLzJBB5YpmUezRua-1920-80.png">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Dune keyboard]]></media:description>                                                            <media:text><![CDATA[Dune keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[Dune keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/crWdvXZLzJBB5YpmUezRua-1920-80.png" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Remember when devices were trying to get rid of buttons, and now more and more companies are working to add AI-centric ones? Step forward, Aina, a company offering something a little unique.</p><p>Its new keypad, Dune, is essentially a small macro pad you can throw in a bag to take on the go with your MacBook. And while I wasn’t sure I needed a context-specific series of buttons attached to my Mac, I couldn’t believe how intuitive it was.</p><div class="product"><a data-dimension112="58063e7a-909c-11f1-a388-2fdc8041c3c9" data-action="Deal Block" data-label="Dune Keypad" data-dimension48="Dune Keypad" data-dimension25="$149" href="https://www.aina.com/waitlist" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1079px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="zgWMsbAydycexBW6cwgCm8" name="Dune Keypad" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/zgWMsbAydycexBW6cwgCm8-1920-80.jpg" mos="" align="middle" fullscreen="" width="1079" height="1079" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Dune keypad attaches directly to your MacBook’s USB-C port to add three fully customizable, context-aware physical buttons to your setup. It automatically swaps hotkey functions depending on your active app, making it easy to mute video calls, trigger Claude AI prompts, or trigger custom macros on the go. If you want Stream Deck-style control without lugging a desktop dock around, it is a smart, portable addition to a mobile workstation.<a class="view-deal button" href="https://www.aina.com/waitlist" target="_blank" rel="nofollow" data-dimension112="58063e7a-909c-11f1-a388-2fdc8041c3c9" data-action="Deal Block" data-label="Dune Keypad" data-dimension48="Dune Keypad" data-dimension25="$149">View Deal</a></p></div><h2 id="get-work-dune">Get work Dune</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:703px;"><p class="vanilla-image-block" style="padding-top:56.19%;"><img id="XxedE3YSqGodmmnTD3AzxM" name="dune keyboard" alt="Dune keyboard" src="https://cdn.mos.cms.futurecdn.net/XxedE3YSqGodmmnTD3AzxM-1920-80.png" mos="" align="middle" fullscreen="" width="703" height="395" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Dune is a small three-key keypad that plugs into your MacBook’s USB-C port on the left-hand side, with three colored, metal buttons, a solid shell that feels built to last, and a very welcome rubberized base to avoid scratching desks and tables.</p><p>Above the buttons is a window showing an LED inside, and my only gripe is that being able to look inside and see some of the circuitry feels like it goes against the rest of the design inspiration here - everything is so clean and fine-tuned that it feels as though the light that goes on when you press a button was a little bit of an afterthought.</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:722px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="X9KUiWyEaD6YtB2UyGxuhh" name="Screenshot 2026-07-31 155517" alt="dune keyboard" src="https://cdn.mos.cms.futurecdn.net/X9KUiWyEaD6YtB2UyGxuhh-1920-80.png" mos="" align="middle" fullscreen="" width="722" height="406" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Still, that’s genuinely my only nitpick here with the design. I have the Midnight MacBook Air (still rocking an M3 chip), and the paint chips like nobody’s business. As a result, I was worried about having an additional metal chassis potentially locked alongside it, but the Dune sits beautifully, with no wiggle from the USB-C port.</p><h2 id="so-what-does-dune-do">So what does Dune do?</h2><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:50.80%;"><img id="7AKC3wwA2kQWyQffoz72f3" name="Dune software 2" alt="Dune keypad" src="https://cdn.mos.cms.futurecdn.net/7AKC3wwA2kQWyQffoz72f3-1920-80.png" mos="" align="middle" fullscreen="" width="748" height="380" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Aina)</span></figcaption></figure><p>Here’s the fun part: Dune basically does anything you need it to. During the setup process, you can flag some key apps you use day-to-day. For my sins, mine include Teams, and Dune instantly offered some useful shortcuts attached to the hardware buttons, all displayed in the bottom-left corner.</p><p>Muting my mic, turning off the camera with a single touch, or simply bringing the meeting to the front of my active windows are all things I have to do regularly, so having the option to hit a clearly defined button for that is ideal.</p><p>Hitting a button to bring up a fresh Claude chat and using the other two buttons to instantly “yay” or “nay” its responses speeds things up by a few seconds each day, which soon adds up across a week, a month, or a year.</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="mdBdzH6SD8SdAT763EYfC3" name="8 - 2026-07-31T155700.784" alt="dune keyboard" src="https://cdn.mos.cms.futurecdn.net/mdBdzH6SD8SdAT763EYfC3-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Dune also cycles its context-specific commands depending on your active window, so pressing the buttons becomes muscle memory.</p><p>At present, the in-app marketplace is still being populated, but there are some solid options already. Being able to open your Mac’s clipboard, or instantly send a screenshot to Claude or update a Slack status are very useful, and I’m excited to see how things pan out from the perspective of power users getting their hands on Dune.</p><p>Sticking with Claude, however, it’s clear this is where Dune’s real appeal lies. In fact, you can build your own workflows for Dune using Claude, be those scripts, app-specific contextual actions, and more. Will you get the same joy from another AI app? It’s hard to say, but if you already have Claude, this is a genuinely intuitive way to build a better Mac.</p><h2 id="a-curio-or-a-must-have">A curio, or a must-have?</h2><p>There are some considerations, of course. For one, if your Mac is docked while you work, then a more traditional macro pad is likely to be more useful. For another, you’ll need a USB-C port free to be able to use Dune.</p><p>And yet, I can see myself packing it for every trip where I’m just taking the laptop and don’t need to plug into a variety of other peripherals.</p><p>For something that must’ve required plenty of thought, it somehow feels so simple to use. Muting meetings, triggering agentic AI, or just having a place to run a shortcut without trying to remember the keyboard combo I set up years ago is genuinely useful.</p><p>Now that I’ve used Dune, I’m very keen to see where it goes from here and how it evolves. As someone who uses a Stream Deck but doesn’t take it on the go, I’m happy to have an alternative that’s much more portable, and that leans nicely into agentic AI.</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/ai/claude/i-just-learned-your-claude-ai-chats-could-show-up-in-google-heres-how-to-check-yours"><strong>I just learned your Claude AI chats could show up in Google — here's how to check yours</strong></a></li><li><a href="https://www.tomsguide.com/ai/5-claude-prompts-to-replace-chatgpt-for-writing-and-analysis"><strong>I stopped using ChatGPT for everything — here are 5 prompts I only run in Claude</strong></a></li><li><a href="https://www.tomsguide.com/ai/i-use-gemini-spark-daily-here-are-7-prompts-that-show-what-it-can-really-do"><strong>I use Gemini Spark daily — here are 7 prompts that show what it can really do</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I spent a week with the Razer Seiren V3 Pro — here’s how it stacks up to my Elgato Wave 3 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’ve been podcasting for the better part of 14 years, and the one piece of gear I’ve invested in more than anything else is one of the <a href="https://www.tomsguide.com/best-picks/best-microphones">best microphones</a>. After all, people will forgive so-so or even boring visuals as long as the audio is solid. I ran a Samson C01U Pro for years before switching to my current <a href="https://www.tomsguide.com/reviews/elgato-wave-3-microphone">Elgato Wave 3</a>, and I’m still happy with it. That said, I’ve been testing a pretty strong rival lately — the Razer Seiren V3 Pro.</p><p>Razer’s latest Seiren is a studio-leaning dynamic mic aimed squarely at podcasters and creators who want cleaner sound without a ton of extra gear. It has a solid, Shure-inspired look and comes packed with built-in processing tools that actually help you sound better right out of the box. At $249, it’s not cheap, but you’re getting dual USB-C and XLR connectivity, a 30 mm capsule, and some useful onboard features for the money.</p><p>So, will I ditch my beloved Wave 3 for the Seiren V3 Pro? Read on for what this mic is all about and how it held up after a week of real use.</p><div class="product"><a data-dimension112="03efcb36-8d21-11f1-9888-91fd9399c585" data-action="Deal Block" data-label="Seiren V3 Pro" data-dimension48="Seiren V3 Pro" data-dimension25="$249" href="https://www.amazon.com/Razer-Seiren-Pro-Microphone-Built/dp/B0H1DW8HXQ" 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:100.00%;"><img id="rqgYtPsERxh7UatykS8Z3k" name="Razer Seiren V3 Pro" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/rqgYtPsERxh7UatykS8Z3k-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Razer Seiren V3 Pro is a microphone built for streamers, podcasters, and creators who want clear, professional audio with minimal fuss. Its cardioid pattern captures focused vocals while reducing background noise, and its onboard processing tools deliver excellent sound out of the box. Though it's not cheap, it'll take your podcasting/content creation to the next level.<a class="view-deal button" href="https://www.amazon.com/Razer-Seiren-Pro-Microphone-Built/dp/B0H1DW8HXQ" target="_blank" rel="nofollow" data-dimension112="03efcb36-8d21-11f1-9888-91fd9399c585" data-action="Deal Block" data-label="Seiren V3 Pro" data-dimension48="Seiren V3 Pro" data-dimension25="$249">View Deal</a></p></div><h2 id="razer-seiren-v3-pro-what-is-it">Razer Seiren V3 Pro: What is it?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5050px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="fDR6d9yR9W7skBDRoKrrGP" name="Razer Seiren V3 Pro-5" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/fDR6d9yR9W7skBDRoKrrGP-1920-80.jpg" mos="" align="middle" fullscreen="" width="5050" height="2841" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Before I dig into my experience with the Seiren V3 Pro, here’s a quick look at the features so you know what you’re dealing with.</p><p>The Seiren V3 Pro is built to deliver the warm, broadcast-style sound many podcasters and streamers are after. Its cardioid pickup pattern means you speak into the top of the mic, which focuses on your voice while rejecting noise from the sides and rear. That’s especially useful if you’re recording in a regular room rather than a treated studio. It also includes a built-in shock absorber and a removable pop filter to minimize noise and plosives before they reach your recording.</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:4734px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WCYE9stSXsy6BZpKU4EEgN" name="Razer Seiren V3 Pro-3" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/WCYE9stSXsy6BZpKU4EEgN-1920-80.jpg" mos="" align="middle" fullscreen="" width="4734" height="2663" attribution="" endorsement="" class="inline"></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 standout features is the dual connectivity. You get USB-C for simple plug-and-play use, plus a full XLR connection when you want to run it through a proper audio interface or mixer. That flexibility means the same mic can sit on a basic streaming desk one day and move into a more serious recording setup the next — no need to buy two different mics!</p><p>On the software side, Razer includes onboard DSP tools such as a compressor, expander, limiter, and an AI noise remover. There’s also 32-bit float support in the Razer Synapse app, which gives you more headroom and less chance of clipping when you get loud. </p><p>Between the processing and the extra dynamic range, much of the cleanup happens before the audio even reaches your computer. A physical gain dial sits right on the mic for quick adjustments, and the Chroma RGB ring serves as both a status indicator (mute, levels, etc.) and a bit of customizable flair.</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:5150px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tAyRBBGGgUwkhLVn4BobnX" name="Razer Seiren V3 Pro-8" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/tAyRBBGGgUwkhLVn4BobnX-1920-80.jpg" mos="" align="middle" fullscreen="" width="5150" height="2897" attribution="" endorsement="" class="inline"></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, the Seiren V3 Pro feels like the best of both worlds: easy enough for more casual users like me to set up and start using, but with enough depth and customization to keep the more serious audio folks happy.</p><h2 id="razer-seiren-v3-pro-my-experience">Razer Seiren V3 Pro: My experience</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4721px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="wMR7YgR4vCdytLdJQo76MU" name="Razer Seiren V3 Pro-2" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/wMR7YgR4vCdytLdJQo76MU-1920-80.jpg" mos="" align="middle" fullscreen="" width="4721" height="2656" attribution="" endorsement="" class="inline"></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 was impressed the second I pulled the Razer Seiren V3 Pro out of the box. It’s a slick-looking piece of kit. Objectively, it doesn’t look all that different from other cardioid mics, but it’s a design style I’ve always liked and rarely had the chance to use. Inside the box, you get the mic, its stand, and a long USB cable. One end is USB-A, and Razer includes a USB-A-to-USB-C adapter, a thoughtful little touch.</p><p>Once you plug it into your PC and open the Razer Synapse app, it guides you through a quick setup. You speak into the mic at a normal volume, record a bit of ambient noise, tell it what you’re using it for (podcasting, in my case), and how far the mic is from you. That’s it. The whole process takes less than five minutes and does much of the heavy lifting for you.</p><p>I’m generally happy with the recording quality. My voice comes through full, and there’s almost no background noise. The only real issue is specific to my setup: the mic sits about two feet away from me, and because I live in a pretty bare studio apartment, it can pick up a bit of that empty-room sound. I don’t have that problem with my Elgato Wave 3 since it’s on an arm and I can pull it right up to my mouth. I can’t do that as easily with the Razer, but even at a less-than-ideal distance, the audio still sounds solid.</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:1919px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="tbbBruAfb26hfJdDc2JEJG" name="Razer Seiren V3 Pro - Synapse" alt="Razer Synapse" src="https://cdn.mos.cms.futurecdn.net/tbbBruAfb26hfJdDc2JEJG-1920-80.jpg" mos="" align="middle" fullscreen="" width="1919" height="1079" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / Razer)</span></figcaption></figure><p>I’m not an audiophile, and I don’t like fussing with settings, so the preset options in Synapse work perfectly fine for me. You can dig in and tweak everything if you want, and I’m sure I could squeeze out even better results with some fine-tuning. For most people, though, the out-of-the-box presets are more than good enough.</p><p>The one real annoyance I encountered is how easy it is to accidentally mute yourself while adjusting the mic. The mute “button” is a large, sensitive target area, and it’s almost impossible not to graze it when repositioning the mic. A proper physical button that requires a bit of force to press would have solved this easily.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-ONq1JW"></div>                            </div>                            <script src="https://kwizly.com/embed/ONq1JW.js" async></script><h2 id="razer-seiren-v3-pro-bottom-line">Razer Seiren V3 Pro: Bottom line</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3865px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xZbxZnEVtx7mXth9G2zUNd" name="Razer Seiren V3 Pro-7" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/xZbxZnEVtx7mXth9G2zUNd-1920-80.jpg" mos="" align="middle" fullscreen="" width="3865" height="2174" attribution="" endorsement="" class="inline"></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 my first time using a Razer microphone, and I have to say that I’m pretty impressed. Between the clean design and the solid set of features, the Seiren V3 Pro feels like a real step up for anyone looking to take their podcasting or content creation more seriously. Yeah, $249 is a chunk of money, and there are cheaper options out there. But if you decide to spend it, I don’t think you’ll walk away disappointed.</p><p>So, back to the question I asked at the start: will I switch from my Elgato Wave 3 to the Razer Seiren V3 Pro? Right now, no. I’d get a lot more out of this mic if I had it on a proper arm so I could keep it closer to my mouth. That said, it’s compact enough that I’ll definitely be tossing it in my bag for podcasting on the road. It’s a fantastic microphone that can make you sound like a pro, and at the end of the day, that’s exactly what I’m after.</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/tvs/yes-your-tv-probably-has-a-microphone-in-it-heres-how-to-turn-it-off">Yes, your TV probably has a microphone in it — here's how to turn it off</a></li><li><a href="https://www.tomsguide.com/best-picks/best-cheap-headphones">The best cheap headphones: big sound for less</a></li><li><a href="https://www.tomsguide.com/audio/microphones/shure-SM4-review">Shure SM4 review: This musician’s microphone is perfect for beginners</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/i-spent-a-week-with-the-razer-seiren-v3-pro-heres-how-it-stacks-up-to-my-elgato-wave-3</link>
                                                                            <description>
                            <![CDATA[ The Razer Seiren V3 Pro is a slick, feature-packed podcasting mic. After a week of testing, can it actually beat the Elgato Wave 3? Here’s my honest take. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">7MYAkP9Ydc8mVwagZiHMyk</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/i5XnJDSFEEDhLa8yBc66wB-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 01 Aug 2026 11:45:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Gaming Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Gaming]]></category>
                                                                                                <author><![CDATA[ tony.polanco@futurenet.com (Tony Polanco) ]]></author>                    <dc:creator><![CDATA[ Tony Polanco ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/atzRNqFt5wYgEUPBDahWsD-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ null ]]></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/i5XnJDSFEEDhLa8yBc66wB-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Razer Seiren V3 Pro]]></media:description>                                                            <media:text><![CDATA[Razer Seiren V3 Pro]]></media:text>
                                <media:title type="plain"><![CDATA[Razer Seiren V3 Pro]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/i5XnJDSFEEDhLa8yBc66wB-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’ve been podcasting for the better part of 14 years, and the one piece of gear I’ve invested in more than anything else is one of the <a href="https://www.tomsguide.com/best-picks/best-microphones">best microphones</a>. After all, people will forgive so-so or even boring visuals as long as the audio is solid. I ran a Samson C01U Pro for years before switching to my current <a href="https://www.tomsguide.com/reviews/elgato-wave-3-microphone">Elgato Wave 3</a>, and I’m still happy with it. That said, I’ve been testing a pretty strong rival lately — the Razer Seiren V3 Pro.</p><p>Razer’s latest Seiren is a studio-leaning dynamic mic aimed squarely at podcasters and creators who want cleaner sound without a ton of extra gear. It has a solid, Shure-inspired look and comes packed with built-in processing tools that actually help you sound better right out of the box. At $249, it’s not cheap, but you’re getting dual USB-C and XLR connectivity, a 30 mm capsule, and some useful onboard features for the money.</p><p>So, will I ditch my beloved Wave 3 for the Seiren V3 Pro? Read on for what this mic is all about and how it held up after a week of real use.</p><div class="product"><a data-dimension112="03efcb36-8d21-11f1-9888-91fd9399c585" data-action="Deal Block" data-label="Seiren V3 Pro" data-dimension48="Seiren V3 Pro" data-dimension25="$249" href="https://www.amazon.com/Razer-Seiren-Pro-Microphone-Built/dp/B0H1DW8HXQ" 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:100.00%;"><img id="rqgYtPsERxh7UatykS8Z3k" name="Razer Seiren V3 Pro" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/rqgYtPsERxh7UatykS8Z3k-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Razer Seiren V3 Pro is a microphone built for streamers, podcasters, and creators who want clear, professional audio with minimal fuss. Its cardioid pattern captures focused vocals while reducing background noise, and its onboard processing tools deliver excellent sound out of the box. Though it's not cheap, it'll take your podcasting/content creation to the next level.<a class="view-deal button" href="https://www.amazon.com/Razer-Seiren-Pro-Microphone-Built/dp/B0H1DW8HXQ" target="_blank" rel="nofollow" data-dimension112="03efcb36-8d21-11f1-9888-91fd9399c585" data-action="Deal Block" data-label="Seiren V3 Pro" data-dimension48="Seiren V3 Pro" data-dimension25="$249">View Deal</a></p></div><h2 id="razer-seiren-v3-pro-what-is-it">Razer Seiren V3 Pro: What is it?</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5050px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="fDR6d9yR9W7skBDRoKrrGP" name="Razer Seiren V3 Pro-5" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/fDR6d9yR9W7skBDRoKrrGP-1920-80.jpg" mos="" align="middle" fullscreen="" width="5050" height="2841" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Before I dig into my experience with the Seiren V3 Pro, here’s a quick look at the features so you know what you’re dealing with.</p><p>The Seiren V3 Pro is built to deliver the warm, broadcast-style sound many podcasters and streamers are after. Its cardioid pickup pattern means you speak into the top of the mic, which focuses on your voice while rejecting noise from the sides and rear. That’s especially useful if you’re recording in a regular room rather than a treated studio. It also includes a built-in shock absorber and a removable pop filter to minimize noise and plosives before they reach your recording.</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:4734px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="WCYE9stSXsy6BZpKU4EEgN" name="Razer Seiren V3 Pro-3" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/WCYE9stSXsy6BZpKU4EEgN-1920-80.jpg" mos="" align="middle" fullscreen="" width="4734" height="2663" attribution="" endorsement="" class="inline"></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 standout features is the dual connectivity. You get USB-C for simple plug-and-play use, plus a full XLR connection when you want to run it through a proper audio interface or mixer. That flexibility means the same mic can sit on a basic streaming desk one day and move into a more serious recording setup the next — no need to buy two different mics!</p><p>On the software side, Razer includes onboard DSP tools such as a compressor, expander, limiter, and an AI noise remover. There’s also 32-bit float support in the Razer Synapse app, which gives you more headroom and less chance of clipping when you get loud. </p><p>Between the processing and the extra dynamic range, much of the cleanup happens before the audio even reaches your computer. A physical gain dial sits right on the mic for quick adjustments, and the Chroma RGB ring serves as both a status indicator (mute, levels, etc.) and a bit of customizable flair.</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:5150px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tAyRBBGGgUwkhLVn4BobnX" name="Razer Seiren V3 Pro-8" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/tAyRBBGGgUwkhLVn4BobnX-1920-80.jpg" mos="" align="middle" fullscreen="" width="5150" height="2897" attribution="" endorsement="" class="inline"></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, the Seiren V3 Pro feels like the best of both worlds: easy enough for more casual users like me to set up and start using, but with enough depth and customization to keep the more serious audio folks happy.</p><h2 id="razer-seiren-v3-pro-my-experience">Razer Seiren V3 Pro: My experience</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:4721px;"><p class="vanilla-image-block" style="padding-top:56.26%;"><img id="wMR7YgR4vCdytLdJQo76MU" name="Razer Seiren V3 Pro-2" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/wMR7YgR4vCdytLdJQo76MU-1920-80.jpg" mos="" align="middle" fullscreen="" width="4721" height="2656" attribution="" endorsement="" class="inline"></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 was impressed the second I pulled the Razer Seiren V3 Pro out of the box. It’s a slick-looking piece of kit. Objectively, it doesn’t look all that different from other cardioid mics, but it’s a design style I’ve always liked and rarely had the chance to use. Inside the box, you get the mic, its stand, and a long USB cable. One end is USB-A, and Razer includes a USB-A-to-USB-C adapter, a thoughtful little touch.</p><p>Once you plug it into your PC and open the Razer Synapse app, it guides you through a quick setup. You speak into the mic at a normal volume, record a bit of ambient noise, tell it what you’re using it for (podcasting, in my case), and how far the mic is from you. That’s it. The whole process takes less than five minutes and does much of the heavy lifting for you.</p><p>I’m generally happy with the recording quality. My voice comes through full, and there’s almost no background noise. The only real issue is specific to my setup: the mic sits about two feet away from me, and because I live in a pretty bare studio apartment, it can pick up a bit of that empty-room sound. I don’t have that problem with my Elgato Wave 3 since it’s on an arm and I can pull it right up to my mouth. I can’t do that as easily with the Razer, but even at a less-than-ideal distance, the audio still sounds solid.</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:1919px;"><p class="vanilla-image-block" style="padding-top:56.23%;"><img id="tbbBruAfb26hfJdDc2JEJG" name="Razer Seiren V3 Pro - Synapse" alt="Razer Synapse" src="https://cdn.mos.cms.futurecdn.net/tbbBruAfb26hfJdDc2JEJG-1920-80.jpg" mos="" align="middle" fullscreen="" width="1919" height="1079" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide / Razer)</span></figcaption></figure><p>I’m not an audiophile, and I don’t like fussing with settings, so the preset options in Synapse work perfectly fine for me. You can dig in and tweak everything if you want, and I’m sure I could squeeze out even better results with some fine-tuning. For most people, though, the out-of-the-box presets are more than good enough.</p><p>The one real annoyance I encountered is how easy it is to accidentally mute yourself while adjusting the mic. The mute “button” is a large, sensitive target area, and it’s almost impossible not to graze it when repositioning the mic. A proper physical button that requires a bit of force to press would have solved this easily.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-ONq1JW"></div>                            </div>                            <script src="https://kwizly.com/embed/ONq1JW.js" async></script><h2 id="razer-seiren-v3-pro-bottom-line">Razer Seiren V3 Pro: Bottom line</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:3865px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xZbxZnEVtx7mXth9G2zUNd" name="Razer Seiren V3 Pro-7" alt="Razer Seiren V3 Pro" src="https://cdn.mos.cms.futurecdn.net/xZbxZnEVtx7mXth9G2zUNd-1920-80.jpg" mos="" align="middle" fullscreen="" width="3865" height="2174" attribution="" endorsement="" class="inline"></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 my first time using a Razer microphone, and I have to say that I’m pretty impressed. Between the clean design and the solid set of features, the Seiren V3 Pro feels like a real step up for anyone looking to take their podcasting or content creation more seriously. Yeah, $249 is a chunk of money, and there are cheaper options out there. But if you decide to spend it, I don’t think you’ll walk away disappointed.</p><p>So, back to the question I asked at the start: will I switch from my Elgato Wave 3 to the Razer Seiren V3 Pro? Right now, no. I’d get a lot more out of this mic if I had it on a proper arm so I could keep it closer to my mouth. That said, it’s compact enough that I’ll definitely be tossing it in my bag for podcasting on the road. It’s a fantastic microphone that can make you sound like a pro, and at the end of the day, that’s exactly what I’m after.</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/tvs/yes-your-tv-probably-has-a-microphone-in-it-heres-how-to-turn-it-off">Yes, your TV probably has a microphone in it — here's how to turn it off</a></li><li><a href="https://www.tomsguide.com/best-picks/best-cheap-headphones">The best cheap headphones: big sound for less</a></li><li><a href="https://www.tomsguide.com/audio/microphones/shure-SM4-review">Shure SM4 review: This musician’s microphone is perfect for beginners</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I meant to use Satechi’s latest accessory at my desk but it turned out to be the perfect travel charger — here’s why ]]></title>
                                                                                                <dc:content><![CDATA[ <p>When packing for a trip — or even worse, a family trip — there are quite a few things you need to remember to bring, especially if you’re going somewhere more remote. Fortunately for the rest of my family, I always remember to bring a charger — several, in fact.</p><p>From towels to pool noodles and everything in between, our car was absolutely packed when we finally pulled out of my driveway. While everyone else made sure to have their beach gear and plenty of extra clothes, I was in charge of electronics per usual.</p><p>Now, we don’t let our four-year-old use a tablet at home, but with a six-hour drive ahead of us, I had no problem making an exception to that rule. So I charged and loaded the <a href="https://www.tomsguide.com/computing/android-tablets/i-thought-smaller-was-better-until-i-tried-a-massive-14-inch-tablet-that-feels-like-a-clipboard">TCL NXTPAPER 14</a> with all of his favorite movies. Even though I consider it one of the <a href="https://www.tomsguide.com/best-picks/best-tablet">best tablets</a>, it doesn’t have a headphone jack, which meant this would be his first time using Bluetooth headphones instead of wired ones.</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/NpgYe6RWhi8" allowfullscreen></iframe></div></div><p>With my son squared away, it was time to pack my own tech bag. After packing my <a href="https://www.tomsguide.com/gaming/nintendo/i-played-star-fox-on-switch-2-in-mouse-mode-and-with-an-n64-controller-heres-the-one-id-recommend">Nintendo Switch 2</a> and a pair of the <a href="https://www.tomsguide.com/computing/vr-ar/best-smart-glasses">best smart glasses</a> so I could <a href="https://www.tomsguide.com/gaming/nintendo/i-played-the-switch-2-with-ar-glasses-heres-what-its-like-to-race-mario-kart-world-on-a-135-inch-screen-in-front-of-your-face">get the big-screen experience on the go</a>, charging was next. While I always bring a plug-in GaN charger to quickly top off all my devices, this time around, I decided to do things a bit differently.</p><p>You see, as I had Satechi’s new ChargeView 140W Desktop Charger waiting on my desk, I threw it in my bag at the last minute — and I’m glad I did. While it’s designed for your desk, it ended up being the perfect charger for our trip. Here’s why.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OLqoKX"></div>                            </div>                            <script src="https://kwizly.com/embed/OLqoKX.js" async></script><h2 id="from-the-wall-to-your-desk">From the wall to your desk</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5607px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="gNjbXfz7eofFfuzB859ggR" name="Satechi ChargeView-6" alt="The Satechi ChargeView 140W Desktop Charger unboxed on a table" src="https://cdn.mos.cms.futurecdn.net/gNjbXfz7eofFfuzB859ggR-1920-80.jpg" mos="" align="middle" fullscreen="" width="5607" height="3154" attribution="" endorsement="" class="inline"></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 3 x 3 x 1.5 inches, Satechi’s new ChargeView Desktop Charger is quite compact. Still, it packs a punch with a total power output of 140W. In the box, you get the charger itself, a power cord, and a stand. Even though it’s meant to be stationary, it’s small enough that you can easily slip it — along with the power cable and stand — into your bag. </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:6000px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="QTKRQ5oJWfiu8brdX9RPqU" name="Satechi ChargeView-5" alt="The power port on the back of the Satechi ChargeView 140W Desktop Charger" src="https://cdn.mos.cms.futurecdn.net/QTKRQ5oJWfiu8brdX9RPqU-1920-80.jpg" mos="" align="middle" fullscreen="" width="6000" height="3376" attribution="" endorsement="" class="inline"></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 things about this particular charger is that it uses a standard, figure-8-style power cord. This means it’s easy to replace, and I guarantee you have more than a few of these lying around your house. In fact, the PS5, Xbox Series X, and even the <a href="https://www.tomsguide.com/computing/desktop-computers/mac-mini-m4-review">Mac mini M4</a> all use this same cord. </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:6000px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="dvkuxMRkWKi4sp7btUREDY" name="Satechi ChargeView-4" alt="The Satechi ChargeView 140W Desktop Charger in a vertical orientation in its included stand" src="https://cdn.mos.cms.futurecdn.net/dvkuxMRkWKi4sp7btUREDY-1920-80.jpg" mos="" align="middle" fullscreen="" width="6000" height="3376" attribution="" endorsement="" class="inline"></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 giving the charger plenty of room to breathe, the included stand has a neat trick up its sleeve. While you can use it with the charger lying flat on its side like I did, you can also set it up vertically. So whether you have horizontal or vertical space to spare on your desk, you’ll be able to find a place for the Satechi ChargeView. Likewise, you won’t struggle to plug it in, since it uses a standard wall plug as opposed to a bulky power adapter that takes up multiple outlets.</p><p>Now back to our family trip, where it proved to be as useful for me as it was for everyone else at our rental property.</p><div class="product"><a data-dimension112="17b2d982-8985-11f1-88a1-69352bf266c1" data-action="Deal Block" data-label="ChargeView 140W Desktop Charger" data-dimension48="ChargeView 140W Desktop Charger" data-dimension25="$99" href="https://satechi.com/products/chargeview-140w-desktop-charger" 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:62.13%;"><img id="Mqvvp2yrTJEcEimwSPiRiR" name="ChargeView 140W Desktop Charger" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Mqvvp2yrTJEcEimwSPiRiR-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="932" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://satechi.com/products/chargeview-140w-desktop-charger" target="_blank" rel="nofollow" data-dimension112="17b2d982-8985-11f1-88a1-69352bf266c1" data-action="Deal Block" data-label="ChargeView 140W Desktop Charger" data-dimension48="ChargeView 140W Desktop Charger" data-dimension25="$99">ChargeView 140W Desktop Charger: $99</a></strong><br>This powerful charger turns a single power outlet into a centralized power hub for your entire desk setup. Powered by advanced GaN technology, it delivers 140W of total power across four high-speed USB-C ports — enough juice to fast-charge laptops, tablets, and accessories simultaneously. Its standout feature is a real-time display that provides live feedback on total wattage draw, per-port output, and thermal status at a glance. Complete with a detachable stand for vertical or flat orientation and an extended power cord, it keeps heavy wall bricks off your outlets and clutter off your workspace. </p><p><strong>Price Check: </strong><a href="https://www.amazon.com/Satechi-ChargeViewTM-Charging-Designed-Workspaces/dp/B0GP57G1FN" target="_blank" rel="nofollow"><strong>$99 @ Amazon</strong></a><a class="view-deal button" href="https://satechi.com/products/chargeview-140w-desktop-charger" target="_blank" rel="nofollow" data-dimension112="17b2d982-8985-11f1-88a1-69352bf266c1" data-action="Deal Block" data-label="ChargeView 140W Desktop Charger" data-dimension48="ChargeView 140W Desktop Charger" data-dimension25="$99">View Deal</a></p></div><h2 id="enough-watts-for-the-whole-family">Enough watts for the whole family</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5669px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LEGwiLnUQLBkevPY2Y72Kd" name="Satechi ChargeView-8" alt="Three phones plugged into the Satechi ChargeView 140W Desktop Charger on an end table next to a couch" src="https://cdn.mos.cms.futurecdn.net/LEGwiLnUQLBkevPY2Y72Kd-1920-80.jpg" mos="" align="middle" fullscreen="" width="5669" height="3189" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Now, this wasn’t my first time staying at this particular Airbnb, as we had actually stayed in the upstairs unit last year. As such, I knew the layout like the back of my hand before we even set off. The living room is right next to the front door as you walk in, and I remembered there were two fairly large end tables on either side of the couch. I figured this would be the perfect place to set up the Satechi ChargeView, since I knew we’d be running back and forth from the beach pretty frequently.</p><p>I plugged the Satechi ChargeView in behind the couch and set it up horizontally toward the back of the end table. Then I grabbed a handful of <a href="https://www.tomsguide.com/computing/software/i-tested-all-my-usb-c-cables-with-this-free-mac-app-instead-of-guessing-each-time-i-plugged-one-in">USB-C to USB-C cables</a> of varying lengths and plugged those in, too. Although I like to plug in my phone and wait until it’s fully charged before picking it up again, I know my wife’s cousin is the type to use his while it’s charging. He is a Zoomer, after all. This is why I plugged a longer cable into the port closest to the couch.</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:6000px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="XBzUJJjGsMeAXgKyAfXd7i" name="Satechi ChargeView-2" alt="A close-up shot of the Satechi ChargeView 140W Desktop Charger's screen while charging" src="https://cdn.mos.cms.futurecdn.net/XBzUJJjGsMeAXgKyAfXd7i-1920-80.jpg" mos="" align="middle" fullscreen="" width="6000" height="3376" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>On the front of the device, there’s a bright display that gives you plenty of charging info at a glance. On the main screen, you can see the total output power and which ports are currently in use. However, pressing the button on top of the Satechi ChargeView lets you drill down for more details. You can see how many watts each connected device is currently pulling and whether the charger is running cool or if its active cooling is currently engaged.</p><p>Now, I’m sure the Satechi ChargeView will be even more useful at my desk back home, but with four people charging everything from phones to tablets to AirPods, it was interesting to see which devices had fast charging and which ones topped off at a trickle. The stand and overall design worked great here, too, since I never had to worry about it coming unplugged. At the same time, this is a charger nobody can borrow and forget to put back — which I know happens quite often on big trips with a lot of people.</p><h2 id="your-charger-their-outlet">Your charger, their outlet</h2><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="N49L6De6GkgFgEXqbQio9K" name="UGREEN Robot GaN 100W-6" alt="The Ugreen Uno Charger 100W on a table next to the Ugreen Nexode 65W charger" src="https://cdn.mos.cms.futurecdn.net/N49L6De6GkgFgEXqbQio9K-1920-80.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>Now don’t get me wrong, I absolutely adore my plug-in GaN chargers, and even with the Satechi ChargeView set up in the living room on our trip, I still had one tucked away in my bag. The problem with these types of chargers isn’t the devices themselves. Instead, it’s where you plug them in, since no two power outlets are the same.</p><p>For instance, the <a href="https://www.tomsguide.com/computing/peripherals/this-gan-charger-let-me-finally-ditch-my-bulky-laptop-charger-and-its-43-off-right-now">Ugreen Nexode 65W</a> on the right in the photo above has a retractable plug. This is great as it makes the whole device more compact, but I’ve definitely been in situations where I just couldn’t get it to stay put in an outlet. Alternatively, the <a href="https://www.tomsguide.com/computing/peripherals/this-robot-gan-charger-is-one-of-my-favorite-gadgets-of-2024-and-it-just-got-a-big-upgrade">Ugreen Uno 100W</a> on the left is a beefier plug-in GaN charger with more wattage and ports. As such, it’s on the heftier side, which has led to a similar situation where I just couldn’t keep it securely plugged into older outlets that have loosened over time.</p><p>Now with the Satechi ChargeView on my recent family trip, I didn’t have to worry about this kind of thing at all. Its long power cord made reaching the nearest outlet a breeze, and its desktop-style design meant I didn’t need to bring extra-long charging cables with me. The display on the front made the whole charging experience a bit more fun, too, since we could all see how fast our devices actually charged.</p><p>After a successful family trip, I’m now tempted to bring the Satechi ChargeView on my next work trip, too — and for good reason. I can unplug it from my desk and grab a spare power cord to quickly throw it in my suitcase. Then once I’ve checked into my hotel, I won’t have to worry about where the outlets are or whether or not they’ll be a good fit for my plug-in GaN chargers. I never meant to bring this desktop-style charger with me, but now that I have, it just might be my new go-to travel charger.</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/ai/i-used-these-5-chatgpt-prompts-to-plan-my-summer-vacation-one-of-them-even-built-a-stress-free-getaway-for-me"><strong>I used these 5 ChatGPT prompts to plan my summer vacation — one of them even built a stress-free getaway for me</strong></a></li><li><a href="https://www.tomsguide.com/wellness/fitness/packing-cubes-are-the-ultimate-travel-hack-and-these-are-my-5-favorites"><strong>Packing cubes are the ultimate travel hack — and these are my 5 favorites</strong></a></li><li><a href="https://www.tomsguide.com/wellness/sleep-tech/5-airport-essentials-for-a-good-nights-sleep-while-traveling"><strong>These 5 airport essentials are the secret to sleeping through long flights — and they start at $14</strong></a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/i-meant-to-use-satechis-latest-accessory-at-my-desk-but-it-turned-out-to-be-the-perfect-travel-charger-heres-why</link>
                                                                            <description>
                            <![CDATA[ Satechi’s ChargeView Desktop Charger is made for desks, but its long cable and built-in display made it an ideal travel charger for a family trip. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">eiwG6GXDqog3BfSVbMxabd</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/E6CgMrXwv6NDhMQD3LJpcG-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 27 Jul 2026 10:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                <author><![CDATA[ anthony.spadafora@futurenet.com (Anthony Spadafora) ]]></author>                    <dc:creator><![CDATA[ Anthony Spadafora ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/z73LEoj7FkUjNG85GcWHtH-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Anthony Spadafora is the managing editor for security and home office furniture at Tom’s Guide where he covers everything from data breaches and malware to password managers and the best way to cover your whole home or business with a strong Wi-Fi signal.&lt;/p&gt;&lt;p&gt;Before joining the team, he spent three years covering cybersecurity and B2B tech for ITProPortal while living in South Korea. After moving back to the US. Anthony joined the TechRadar Pro team where he covered these topics along with VPNs, web hosting, online collaboration software and video conferencing for four years. Anthony also has his ears to the ground and is on the lookout for the next major cyberattack or data breach.&lt;/p&gt;&lt;p&gt;Based in Houston, Texas, Anthony also handles VPN testing for both Tom’s Guide and TechRadar. As someone who has worked from home exclusively since 2018, he has reviewed dozens of standing desks as well as office chairs and has taken a closer look at other essential remote working accessories. As part of these reviews, Anthony frequently builds intricate desk setups which is why he’s such a big advocate for cable management and keeping things organized. When he’s not writing, he can be found tinkering with PCs and game consoles, managing cables and making upgrades to his smart home.&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/E6CgMrXwv6NDhMQD3LJpcG-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The Satechi ChargeView 140W Desktop Charger on an end table next to a nautical-themed lamp]]></media:description>                                                            <media:text><![CDATA[The Satechi ChargeView 140W Desktop Charger on an end table next to a nautical-themed lamp]]></media:text>
                                <media:title type="plain"><![CDATA[The Satechi ChargeView 140W Desktop Charger on an end table next to a nautical-themed lamp]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/E6CgMrXwv6NDhMQD3LJpcG-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>When packing for a trip — or even worse, a family trip — there are quite a few things you need to remember to bring, especially if you’re going somewhere more remote. Fortunately for the rest of my family, I always remember to bring a charger — several, in fact.</p><p>From towels to pool noodles and everything in between, our car was absolutely packed when we finally pulled out of my driveway. While everyone else made sure to have their beach gear and plenty of extra clothes, I was in charge of electronics per usual.</p><p>Now, we don’t let our four-year-old use a tablet at home, but with a six-hour drive ahead of us, I had no problem making an exception to that rule. So I charged and loaded the <a href="https://www.tomsguide.com/computing/android-tablets/i-thought-smaller-was-better-until-i-tried-a-massive-14-inch-tablet-that-feels-like-a-clipboard">TCL NXTPAPER 14</a> with all of his favorite movies. Even though I consider it one of the <a href="https://www.tomsguide.com/best-picks/best-tablet">best tablets</a>, it doesn’t have a headphone jack, which meant this would be his first time using Bluetooth headphones instead of wired ones.</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/NpgYe6RWhi8" allowfullscreen></iframe></div></div><p>With my son squared away, it was time to pack my own tech bag. After packing my <a href="https://www.tomsguide.com/gaming/nintendo/i-played-star-fox-on-switch-2-in-mouse-mode-and-with-an-n64-controller-heres-the-one-id-recommend">Nintendo Switch 2</a> and a pair of the <a href="https://www.tomsguide.com/computing/vr-ar/best-smart-glasses">best smart glasses</a> so I could <a href="https://www.tomsguide.com/gaming/nintendo/i-played-the-switch-2-with-ar-glasses-heres-what-its-like-to-race-mario-kart-world-on-a-135-inch-screen-in-front-of-your-face">get the big-screen experience on the go</a>, charging was next. While I always bring a plug-in GaN charger to quickly top off all my devices, this time around, I decided to do things a bit differently.</p><p>You see, as I had Satechi’s new ChargeView 140W Desktop Charger waiting on my desk, I threw it in my bag at the last minute — and I’m glad I did. While it’s designed for your desk, it ended up being the perfect charger for our trip. Here’s why.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OLqoKX"></div>                            </div>                            <script src="https://kwizly.com/embed/OLqoKX.js" async></script><h2 id="from-the-wall-to-your-desk">From the wall to your desk</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5607px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="gNjbXfz7eofFfuzB859ggR" name="Satechi ChargeView-6" alt="The Satechi ChargeView 140W Desktop Charger unboxed on a table" src="https://cdn.mos.cms.futurecdn.net/gNjbXfz7eofFfuzB859ggR-1920-80.jpg" mos="" align="middle" fullscreen="" width="5607" height="3154" attribution="" endorsement="" class="inline"></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 3 x 3 x 1.5 inches, Satechi’s new ChargeView Desktop Charger is quite compact. Still, it packs a punch with a total power output of 140W. In the box, you get the charger itself, a power cord, and a stand. Even though it’s meant to be stationary, it’s small enough that you can easily slip it — along with the power cable and stand — into your bag. </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:6000px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="QTKRQ5oJWfiu8brdX9RPqU" name="Satechi ChargeView-5" alt="The power port on the back of the Satechi ChargeView 140W Desktop Charger" src="https://cdn.mos.cms.futurecdn.net/QTKRQ5oJWfiu8brdX9RPqU-1920-80.jpg" mos="" align="middle" fullscreen="" width="6000" height="3376" attribution="" endorsement="" class="inline"></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 things about this particular charger is that it uses a standard, figure-8-style power cord. This means it’s easy to replace, and I guarantee you have more than a few of these lying around your house. In fact, the PS5, Xbox Series X, and even the <a href="https://www.tomsguide.com/computing/desktop-computers/mac-mini-m4-review">Mac mini M4</a> all use this same cord. </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:6000px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="dvkuxMRkWKi4sp7btUREDY" name="Satechi ChargeView-4" alt="The Satechi ChargeView 140W Desktop Charger in a vertical orientation in its included stand" src="https://cdn.mos.cms.futurecdn.net/dvkuxMRkWKi4sp7btUREDY-1920-80.jpg" mos="" align="middle" fullscreen="" width="6000" height="3376" attribution="" endorsement="" class="inline"></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 giving the charger plenty of room to breathe, the included stand has a neat trick up its sleeve. While you can use it with the charger lying flat on its side like I did, you can also set it up vertically. So whether you have horizontal or vertical space to spare on your desk, you’ll be able to find a place for the Satechi ChargeView. Likewise, you won’t struggle to plug it in, since it uses a standard wall plug as opposed to a bulky power adapter that takes up multiple outlets.</p><p>Now back to our family trip, where it proved to be as useful for me as it was for everyone else at our rental property.</p><div class="product"><a data-dimension112="17b2d982-8985-11f1-88a1-69352bf266c1" data-action="Deal Block" data-label="ChargeView 140W Desktop Charger" data-dimension48="ChargeView 140W Desktop Charger" data-dimension25="$99" href="https://satechi.com/products/chargeview-140w-desktop-charger" 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:62.13%;"><img id="Mqvvp2yrTJEcEimwSPiRiR" name="ChargeView 140W Desktop Charger" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Mqvvp2yrTJEcEimwSPiRiR-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="932" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://satechi.com/products/chargeview-140w-desktop-charger" target="_blank" rel="nofollow" data-dimension112="17b2d982-8985-11f1-88a1-69352bf266c1" data-action="Deal Block" data-label="ChargeView 140W Desktop Charger" data-dimension48="ChargeView 140W Desktop Charger" data-dimension25="$99">ChargeView 140W Desktop Charger: $99</a></strong><br>This powerful charger turns a single power outlet into a centralized power hub for your entire desk setup. Powered by advanced GaN technology, it delivers 140W of total power across four high-speed USB-C ports — enough juice to fast-charge laptops, tablets, and accessories simultaneously. Its standout feature is a real-time display that provides live feedback on total wattage draw, per-port output, and thermal status at a glance. Complete with a detachable stand for vertical or flat orientation and an extended power cord, it keeps heavy wall bricks off your outlets and clutter off your workspace. </p><p><strong>Price Check: </strong><a href="https://www.amazon.com/Satechi-ChargeViewTM-Charging-Designed-Workspaces/dp/B0GP57G1FN" target="_blank" rel="nofollow"><strong>$99 @ Amazon</strong></a><a class="view-deal button" href="https://satechi.com/products/chargeview-140w-desktop-charger" target="_blank" rel="nofollow" data-dimension112="17b2d982-8985-11f1-88a1-69352bf266c1" data-action="Deal Block" data-label="ChargeView 140W Desktop Charger" data-dimension48="ChargeView 140W Desktop Charger" data-dimension25="$99">View Deal</a></p></div><h2 id="enough-watts-for-the-whole-family">Enough watts for the whole family</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:5669px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="LEGwiLnUQLBkevPY2Y72Kd" name="Satechi ChargeView-8" alt="Three phones plugged into the Satechi ChargeView 140W Desktop Charger on an end table next to a couch" src="https://cdn.mos.cms.futurecdn.net/LEGwiLnUQLBkevPY2Y72Kd-1920-80.jpg" mos="" align="middle" fullscreen="" width="5669" height="3189" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Now, this wasn’t my first time staying at this particular Airbnb, as we had actually stayed in the upstairs unit last year. As such, I knew the layout like the back of my hand before we even set off. The living room is right next to the front door as you walk in, and I remembered there were two fairly large end tables on either side of the couch. I figured this would be the perfect place to set up the Satechi ChargeView, since I knew we’d be running back and forth from the beach pretty frequently.</p><p>I plugged the Satechi ChargeView in behind the couch and set it up horizontally toward the back of the end table. Then I grabbed a handful of <a href="https://www.tomsguide.com/computing/software/i-tested-all-my-usb-c-cables-with-this-free-mac-app-instead-of-guessing-each-time-i-plugged-one-in">USB-C to USB-C cables</a> of varying lengths and plugged those in, too. Although I like to plug in my phone and wait until it’s fully charged before picking it up again, I know my wife’s cousin is the type to use his while it’s charging. He is a Zoomer, after all. This is why I plugged a longer cable into the port closest to the couch.</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:6000px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="XBzUJJjGsMeAXgKyAfXd7i" name="Satechi ChargeView-2" alt="A close-up shot of the Satechi ChargeView 140W Desktop Charger's screen while charging" src="https://cdn.mos.cms.futurecdn.net/XBzUJJjGsMeAXgKyAfXd7i-1920-80.jpg" mos="" align="middle" fullscreen="" width="6000" height="3376" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>On the front of the device, there’s a bright display that gives you plenty of charging info at a glance. On the main screen, you can see the total output power and which ports are currently in use. However, pressing the button on top of the Satechi ChargeView lets you drill down for more details. You can see how many watts each connected device is currently pulling and whether the charger is running cool or if its active cooling is currently engaged.</p><p>Now, I’m sure the Satechi ChargeView will be even more useful at my desk back home, but with four people charging everything from phones to tablets to AirPods, it was interesting to see which devices had fast charging and which ones topped off at a trickle. The stand and overall design worked great here, too, since I never had to worry about it coming unplugged. At the same time, this is a charger nobody can borrow and forget to put back — which I know happens quite often on big trips with a lot of people.</p><h2 id="your-charger-their-outlet">Your charger, their outlet</h2><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="N49L6De6GkgFgEXqbQio9K" name="UGREEN Robot GaN 100W-6" alt="The Ugreen Uno Charger 100W on a table next to the Ugreen Nexode 65W charger" src="https://cdn.mos.cms.futurecdn.net/N49L6De6GkgFgEXqbQio9K-1920-80.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>Now don’t get me wrong, I absolutely adore my plug-in GaN chargers, and even with the Satechi ChargeView set up in the living room on our trip, I still had one tucked away in my bag. The problem with these types of chargers isn’t the devices themselves. Instead, it’s where you plug them in, since no two power outlets are the same.</p><p>For instance, the <a href="https://www.tomsguide.com/computing/peripherals/this-gan-charger-let-me-finally-ditch-my-bulky-laptop-charger-and-its-43-off-right-now">Ugreen Nexode 65W</a> on the right in the photo above has a retractable plug. This is great as it makes the whole device more compact, but I’ve definitely been in situations where I just couldn’t get it to stay put in an outlet. Alternatively, the <a href="https://www.tomsguide.com/computing/peripherals/this-robot-gan-charger-is-one-of-my-favorite-gadgets-of-2024-and-it-just-got-a-big-upgrade">Ugreen Uno 100W</a> on the left is a beefier plug-in GaN charger with more wattage and ports. As such, it’s on the heftier side, which has led to a similar situation where I just couldn’t keep it securely plugged into older outlets that have loosened over time.</p><p>Now with the Satechi ChargeView on my recent family trip, I didn’t have to worry about this kind of thing at all. Its long power cord made reaching the nearest outlet a breeze, and its desktop-style design meant I didn’t need to bring extra-long charging cables with me. The display on the front made the whole charging experience a bit more fun, too, since we could all see how fast our devices actually charged.</p><p>After a successful family trip, I’m now tempted to bring the Satechi ChargeView on my next work trip, too — and for good reason. I can unplug it from my desk and grab a spare power cord to quickly throw it in my suitcase. Then once I’ve checked into my hotel, I won’t have to worry about where the outlets are or whether or not they’ll be a good fit for my plug-in GaN chargers. I never meant to bring this desktop-style charger with me, but now that I have, it just might be my new go-to travel charger.</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/ai/i-used-these-5-chatgpt-prompts-to-plan-my-summer-vacation-one-of-them-even-built-a-stress-free-getaway-for-me"><strong>I used these 5 ChatGPT prompts to plan my summer vacation — one of them even built a stress-free getaway for me</strong></a></li><li><a href="https://www.tomsguide.com/wellness/fitness/packing-cubes-are-the-ultimate-travel-hack-and-these-are-my-5-favorites"><strong>Packing cubes are the ultimate travel hack — and these are my 5 favorites</strong></a></li><li><a href="https://www.tomsguide.com/wellness/sleep-tech/5-airport-essentials-for-a-good-nights-sleep-while-traveling"><strong>These 5 airport essentials are the secret to sleeping through long flights — and they start at $14</strong></a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Upgrade your home WiFi with this eero mesh router deal ]]></title>
                                                                                                <dc:content><![CDATA[ <p>With back to school season underway, there are plenty of affordable mesh routers you can buy. However, when it comes to value — we're big fans of the Amazon eero 6+. </p><p>For a limited time, you can get the <a href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow">Amazon eero6+ Three-Pack for just $194 at Best Buy</a>. That's the cheapest it's ever been and one of my favorite <a href="https://www.tomsguide.com/sales-events/i-vetted-hundreds-of-best-buy-black-friday-in-july-sales-heres-the-33-id-pick">Best Buy Black Friday in July</a> sales this week.</p><div class="product"><a data-dimension112="8c72ae3a-86bc-11f1-99fa-b1d5550202aa" data-action="Deal Block" data-label="6+ (Three-Pack)" data-dimension48="6+ (Three-Pack)" data-dimension25="$194" href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:520px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="VZyDEv6S8xkrcZLSnsqHij" name="eero.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/VZyDEv6S8xkrcZLSnsqHij-1920-80.jpg" mos="" align="middle" fullscreen="" width="520" height="520" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow" data-dimension112="8c72ae3a-86bc-11f1-99fa-b1d5550202aa" data-action="Deal Block" data-label="6+ (Three-Pack)" data-dimension48="6+ (Three-Pack)" data-dimension25="$194">6+ (Three-Pack): was $299 now $194 at Best Buy</a></strong><br>The Eero 6+ is not only Amazon's smallest mesh Wi-Fi system, it's also the fastest one with Wi-Fi 6 support. An upgrade to the original Eero 6, it now uses faster 160 MHz data channels for better speeds. On the back of each interchangeable unit, you get two gigabit Ethernet ports. Like with other Eero devices, you can manage your network, download and install updates and more using the Eero app. Likewise, you can upgrade to <a href="https://www.tomsguide.com/how-to/how-to-set-up-eero-internet-backup-for-your-eero-router">Eero Plus</a> for additional security features and access to a VPN and password manager.<a class="view-deal button" href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow" data-dimension112="8c72ae3a-86bc-11f1-99fa-b1d5550202aa" data-action="Deal Block" data-label="6+ (Three-Pack)" data-dimension48="6+ (Three-Pack)" data-dimension25="$194">View Deal</a></p></div><p>This Wi-Fi 6-powered mesh system can cover up to 4,500 square feet and supports 75+ connected devices. We like that the eero 6+ units are compact and easy to hide around your home. They're also a breeze to set up thanks to the easy-to-use eero app. Since eero is owned by Amazon, this mesh kit works great with your Alexa devices and you can even use an Echo Dot as a mesh extender if you need extra coverage for your home network.</p><p>It's not uncommon to find this 3-pack on sale, but right now it's selling at full retail price at Amazon, which makes Best Buy's deal that much better. This is also the cheapest it's ever been, so you know you're getting a solid bargain. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/upgrade-your-home-wifi-with-this-eero-mesh-router-deal</link>
                                                                            <description>
                            <![CDATA[ The eero 6+ is a Wi-Fi 6-powered mesh system that can cover up to 4,500 square feet and supports 75+ connected devices. It's now on sale at Best Buy. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">jtYyd3DKT5Jfdj34EEuQzS</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/ntgzcvyYfZ7R8jBcpoK7F9-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 23 Jul 2026 17:33:08 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                <author><![CDATA[ louis.ramirez@futurenet.com (Louis Ramirez) ]]></author>                    <dc:creator><![CDATA[ Louis Ramirez ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/oYF2xVwCb4pTGhAJi8Yqr5-320-70.jpeg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;As deals editor-in-chief at Tom’s Guide, Louis refuses to pay full price for anything. He has over 10 years of experience finding the best deals and coupons to bring readers. Louis price checks against multiple retailers and searches high and low for sales on 4K TVs, Apple devices, streaming services, mattresses, kitchen appliances, and more. He&#039;s also always looking for the the best coupon codes to turn good deals into stellar buys.&lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;He has covered major retail events like Amazon Prime Day, Black Friday, and Cyber Monday for more than a decade and knows how to spot an epic deal from a fake discount. He&#039;s also great at helping people negotiate better prices on everything from gym memberships to their home internet service. Louis&#039; work has also helped readers find PS5 restock, baby formula, Clorox wipes, and everything in between. For his next major purchase, Louis is hoping to find a killer deal on an M3-based Mac. &lt;/p&gt;&lt;p&gt;&lt;br&gt;&lt;/p&gt;&lt;p&gt;A born-and-bred New Yorker, Louis is always in search of the perfect cup of coffee. On his free time, he enjoys swimming, running, and any workout that pushes his Apple Watch to the max. He&#039;s also a big fan of international films, horror movies, and K-dramas. His work has appeared on Gizmodo, CNET, Digital Trends, Dealnews, and Time Out New York.&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/ntgzcvyYfZ7R8jBcpoK7F9-1920-80.jpg">
                                                            <media:credit><![CDATA[Amazon edited with Gemini]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Eero 6+ three pack]]></media:description>                                                            <media:text><![CDATA[Eero 6+ three pack]]></media:text>
                                <media:title type="plain"><![CDATA[Eero 6+ three pack]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/ntgzcvyYfZ7R8jBcpoK7F9-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>With back to school season underway, there are plenty of affordable mesh routers you can buy. However, when it comes to value — we're big fans of the Amazon eero 6+. </p><p>For a limited time, you can get the <a href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow">Amazon eero6+ Three-Pack for just $194 at Best Buy</a>. That's the cheapest it's ever been and one of my favorite <a href="https://www.tomsguide.com/sales-events/i-vetted-hundreds-of-best-buy-black-friday-in-july-sales-heres-the-33-id-pick">Best Buy Black Friday in July</a> sales this week.</p><div class="product"><a data-dimension112="8c72ae3a-86bc-11f1-99fa-b1d5550202aa" data-action="Deal Block" data-label="6+ (Three-Pack)" data-dimension48="6+ (Three-Pack)" data-dimension25="$194" href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:520px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="VZyDEv6S8xkrcZLSnsqHij" name="eero.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/VZyDEv6S8xkrcZLSnsqHij-1920-80.jpg" mos="" align="middle" fullscreen="" width="520" height="520" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow" data-dimension112="8c72ae3a-86bc-11f1-99fa-b1d5550202aa" data-action="Deal Block" data-label="6+ (Three-Pack)" data-dimension48="6+ (Three-Pack)" data-dimension25="$194">6+ (Three-Pack): was $299 now $194 at Best Buy</a></strong><br>The Eero 6+ is not only Amazon's smallest mesh Wi-Fi system, it's also the fastest one with Wi-Fi 6 support. An upgrade to the original Eero 6, it now uses faster 160 MHz data channels for better speeds. On the back of each interchangeable unit, you get two gigabit Ethernet ports. Like with other Eero devices, you can manage your network, download and install updates and more using the Eero app. Likewise, you can upgrade to <a href="https://www.tomsguide.com/how-to/how-to-set-up-eero-internet-backup-for-your-eero-router">Eero Plus</a> for additional security features and access to a VPN and password manager.<a class="view-deal button" href="https://bestbuy.7tiv.net/c/1943169/614286/10014?subId1=hawk-custom-tracking&u=https%3A%2F%2Fwww.bestbuy.com%2Fproduct%2Feero-6-dual-band-mesh-wi-fi-6-system-3-pack-white%2FJ39QV887TJ%2Fsku%2F6495773&ccid=8407&postid=8407-166476-114548&sowid=166476&taskid=114548" target="_blank" rel="nofollow" data-dimension112="8c72ae3a-86bc-11f1-99fa-b1d5550202aa" data-action="Deal Block" data-label="6+ (Three-Pack)" data-dimension48="6+ (Three-Pack)" data-dimension25="$194">View Deal</a></p></div><p>This Wi-Fi 6-powered mesh system can cover up to 4,500 square feet and supports 75+ connected devices. We like that the eero 6+ units are compact and easy to hide around your home. They're also a breeze to set up thanks to the easy-to-use eero app. Since eero is owned by Amazon, this mesh kit works great with your Alexa devices and you can even use an Echo Dot as a mesh extender if you need extra coverage for your home network.</p><p>It's not uncommon to find this 3-pack on sale, but right now it's selling at full retail price at Amazon, which makes Best Buy's deal that much better. This is also the cheapest it's ever been, so you know you're getting a solid bargain. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I thought this comically oversized desk button was an April Fools' joke — but now slamming it down is my favorite workday distraction ]]></title>
                                                                                                <dc:content><![CDATA[ <p>There comes a time when doing things easier and faster becomes your sole motivation and the driving force behind learning new things on your computer. </p><p>Whether that’s doing complex Photoshop edits in fewer clicks or mastering even the most obscure <a href="https://www.tomsguide.com/computing/the-internet-is-sharing-its-favorite-keyboard-shortcuts-and-i-cant-believe-im-just-learning-some-of-these">keyboard shortcuts</a>, once you fall down that rabbit hole, it seems like you’ll never reach the bottom.</p><p>Like with most people, this started small for me. A keyboard shortcut here and a keyboard shortcut there, until I was outright remapping them to buttons on my mouse. Then I took things a step further and did so <a href="https://www.tomsguide.com/opinion/i-used-this-tiny-game-controller-as-a-productivity-tool-for-work-heres-what-happened">with a tiny controller</a> from 8BitDo and <a href="https://www.tomsguide.com/opinion/i-tried-using-a-foot-pedal-to-increase-my-productivity-at-work-heres-what-happened">even a foot pedal</a> connected to my PC. However, there was an easier way: macropads.</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/njtcgcqZWXk" allowfullscreen></iframe></div></div><p>Designed to work alongside your keyboard and mouse, macropads are fully customizable accessories where you can map whatever you want to each key. From simple things like copy and paste to full-on commands, the possibilities are truly endless. However, what if you only had a single, comically large key to work with?</p><p>Well, that’s exactly the position I found myself in when I decided to add Keychron’s Q0 Mini 8K Action Key to my desk setup. While a traditional macropad certainly would have been more useful, reaching over to slam this one down throughout my workday has been a whole lot more fun. </p><div class="product"><a data-dimension112="714be9ba-820b-11f1-8e2c-4f2cd85e9bbb" data-action="Deal Block" data-label="Q0 Mini 8K Action Key" data-dimension48="Q0 Mini 8K Action Key" data-dimension25="$65" href="https://www.keychron.com/products/keychron-q0-mini-8k-action-key" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1214px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="kPxKMVufz87tSrXaZa6dbT" name="Q0 Mini 8K Action Key" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/kPxKMVufz87tSrXaZa6dbT-1920-80.webp" mos="" align="middle" fullscreen="" width="1214" height="1214" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.keychron.com/products/keychron-q0-mini-8k-action-key" target="_blank" rel="nofollow" data-dimension112="714be9ba-820b-11f1-8e2c-4f2cd85e9bbb" data-action="Deal Block" data-label="Q0 Mini 8K Action Key" data-dimension48="Q0 Mini 8K Action Key" data-dimension25="$65">Q0 Mini 8K Action Key: $65</a></strong><br>This comically oversized, single-key macropad features a blazing-fast 8K polling rate for near-instant inputs and an ultra-durable machined aluminum base. You can completely customize its single action key or record complex multi-key macros using the browser-based Keychron Launcher too. In addition to a custom, giant 64x Outemu mechanical switch under the hood, the Q0 also features RGB lighting you can tweak to your liking.<a class="view-deal button" href="https://www.keychron.com/products/keychron-q0-mini-8k-action-key" target="_blank" rel="nofollow" data-dimension112="714be9ba-820b-11f1-8e2c-4f2cd85e9bbb" data-action="Deal Block" data-label="Q0 Mini 8K Action Key" data-dimension48="Q0 Mini 8K Action Key" data-dimension25="$65">View Deal</a></p></div><h2 id="not-your-typical-keychron-keyboard">Not your typical Keychron keyboard</h2><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="KB9umEgX6x6g7qQshK87EH" name="Keychron-Q0--7" alt="The Keychron Q0 Mini 8K Action Key unboxed on a table" src="https://cdn.mos.cms.futurecdn.net/KB9umEgX6x6g7qQshK87EH-1920-80.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>From a <a href="https://www.tomsguide.com/computing/peripherals/i-spent-just-dollar30-on-a-mechanical-keyboard-to-prove-you-dont-need-to-waste-your-money">super budget-friendly wired board</a> to a <a href="https://www.tomsguide.com/computing/keyboards/i-used-keychrons-new-split-mechanical-keyboard-for-a-week-and-its-tether-free-design-is-a-complete-game-changer">split ergonomic one</a>, I’ve tried out a lot of Keychron’s keyboards over the past few years. In fact, I’d go so far as to say that the company makes some of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> you can get today without breaking the bank. Keychron’s Q0 Mini 8K Action Key may only have a single key switch, but unboxing it still gave me the familiar experience I’ve come to expect from the brand.</p><p>In the box, you get the Q0 Mini 8K Action Key itself along with a braided USB-C cable, and just in case you don’t have any free USB-C ports, you also get a USB-A adapter. For a device that could have easily been an <a href="https://www.tomsguide.com/news/live/april-fools-day-2026-live-best-jokes-pranks">April Fools' joke</a>, everything feels a whole lot more premium than it has any right to be.</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="yPFtHUboHAH7uHuXBJ6w3L" name="Keychron-Q0--6" alt="The USB-C port at the back of the Keychron Q0 Mini 8K Action Key" src="https://cdn.mos.cms.futurecdn.net/yPFtHUboHAH7uHuXBJ6w3L-1920-80.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 first thing that stuck out to me about the Q0 — besides the fact that it’s massive at 3.65 x 3.25 x 3.25 inches — is its heft. This thing isn’t going anywhere on your desk thanks to its machined aluminum base. Even with its oversized keycap, the Q0 weighs nearly a pound on its own.</p><p>On the backside, there’s a single, recessed USB-C port and that’s all you get. While I would have loved for it to have Bluetooth or wireless 2.4 GHz connectivity, Keychron went overboard with its wired connection. A single key like this doesn’t need to be that fast, but the company went ahead and gave it a polling rate of 8,000 Hz anyway, which I consider overkill in the best possible way.</p><p>I have to admit, I didn’t read the instructions as I was just too excited to try out this single-key macropad for giants. But before I took it over to my desk and plugged it in, there was one more thing I wanted to try, and boy am I glad I did.</p><h2 id="one-key-to-rule-them-all">One key to rule them all</h2><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="4ZiE2aiR3DBhmdG8mbCsiN" name="Keychron-Q0--5" alt="The Keychron Q0 Mini 8K Action Key with its keycap removed" src="https://cdn.mos.cms.futurecdn.net/4ZiE2aiR3DBhmdG8mbCsiN-1920-80.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>Unlike those cheap membrane keyboards you find in offices or libraries, one of my favorite things about mechanical keyboards is that they’re just as easy to clean as they are to customize. And that all comes down to the fact that with a bit of back and forth, their keycaps pop right off.</p><p>Now if the Q0’s keycap had been permanently attached to the switch underneath it, I wouldn’t have knocked Keychron for it in the slightest. Much to my surprise though, when I gave its double-shot PBT keycap — with a rocket ship emblazoned on top — a bit of a wiggle, it popped right off. Oh, and if you don’t like rockets, Keychron has the <a href="https://www.keychron.com/pages/multi-view-drawing-keychron-q0-mini-8k-action-key" target="_blank">switch and keycap spec drawing</a> on its site so you can make your own with one of the <a href="https://www.tomsguide.com/us/best-3d-printers,review-2236.html">best 3D printers</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="hXNLRhSjXnRbr5GiVuZmYS" name="Keychron-Q0--4" alt="A closeup shot of the key switch on the Keychron Q0 Mini 8K Action Key" src="https://cdn.mos.cms.futurecdn.net/hXNLRhSjXnRbr5GiVuZmYS-1920-80.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>Not only did this let me see the inner workings of this extra-large 3 x 2.5-inch keycap, but I also got a closer look at the mechanical key switch underneath it. By working closely with Outemu, Keychron created a scaled-up version of a traditional key switch that’s actually 64 times larger. It has your standard MX-style cross stem and a south-facing LED for the RGB goodness often associated with mechanical keyboards.</p><p>I’ve been using a mechanical keyboard for over a decade now, but I wish there was something like this when I first got into the hobby. It’s one thing to see a diagram of how all the various types of <a href="https://www.tomsguide.com/computing/peripherals/i-made-a-huge-mistake-with-my-new-mechanical-keyboard-but-this-feature-saved-the-day">mechanical key switches</a> work and it’s another thing to hold an oversized one in your hands.</p><p>With my curiosity piqued, it was time to plug the Q0 in and see if a massive macropad was as fun to use as it was to hold in my hands. Spoiler alert: it definitely is.</p><h2 id="bottoming-out-never-felt-this-good">Bottoming out never felt this 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:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uJkyhhipskP3QXg5KeFQ2Y" name="Keychron-Q0--2" alt="The Keychron Q0 Mini 8K Action Key on the edge of a desk" src="https://cdn.mos.cms.futurecdn.net/uJkyhhipskP3QXg5KeFQ2Y-1920-80.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>Next to the mechanical keyboard on my desk, the Q0 looks almost absurd. It’s about the size of a coaster, and unlike the rest of the peripherals in my current desk setup, it’s the only wired one. </p><p>Still, being able to reach over and slam it down to pull off pretty much anything I can think of is absolutely worth it. And for the keen-eyed among you, yes, the keyboard I’m currently using — the <a href="https://www.tomsguide.com/computing/keyboards/i-used-keychrons-new-split-mechanical-keyboard-for-a-week-and-its-tether-free-design-is-a-complete-game-changer">Keychron Q11 Ultra</a> — already has five programmable macro keys.</p><p>With the Q0 plugged into the <a href="https://www.tomsguide.com/computing/mini-pcs/geekom-a9-max-review">Geekom A9 Max</a> mini PC under my monitor, I pressed it and ... nothing happened. Yeah, I was a little surprised at first, too. I figured the Q0’s massive key would be mapped to Esc or Enter out of the box, but that wasn’t the case. This is a device for tinkerers and mechanical keyboard enthusiasts after all, which is why Keychron likely decided to ship it unmapped.</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:3725px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="vd84JZZCNUnugLNxNougsb" name="keychron-launcher-q0-custom-keys" alt="A screenshot showing how you can customize the Keychron Q0 Mini 8K Action Key using the Keychron Launcher" src="https://cdn.mos.cms.futurecdn.net/vd84JZZCNUnugLNxNougsb-1920-80.jpg" mos="" align="middle" fullscreen="" width="3725" height="2095" attribution="" endorsement="" class="inline"></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 with the rest of Keychron’s more recent keyboards and mice, complete customization is just a browser tab away. So I loaded up the <a href="https://launcher.keychron.com/" target="_blank">Keychron Launcher</a> in Chrome and the Q0 was instantly recognized. Normally in this browser-based launcher, you’re treated to a whole keyboard’s worth of keys you can tweak and customize to your liking. Here though, as you’d expect, there’s only one key to work with.</p><p>Sure, you could program it to Esc, Enter, or even the Windows key, but where’s the fun in that? Likewise, since it’s compatible with Mac (and Linux), you could have it work as a giant Command key too. However, I immediately went to the Special Keys option in the Keychron Launcher and saw what was available for each platform.</p><p>If I wanted to lose my work every time my 4-year-old came to my desk, I could have easily programmed the Q0 to shut down my computer, but I’m not a glutton for punishment. Alternatively, it would also work quite well as a massive mute/unmute button for video calls. </p><p>Remember what I said before about fewer clicks? Well, opening File Explorer on Windows is something I do quite often, and who wants to mouse down to their taskbar or open the Start Menu? After trying out a few different customizations with the Q0, I settled on having it open a new File Explorer window.</p><p>At the same time, you can take things even further by recording your own macros — think complex multi-key commands — and programming them to the Q0. I wanted to keep things simple though, so I’ve mainly stuck to File Explorer for now. Still, it’s great knowing that when I do think of a unique new use case or come up with a clever macro, reprogramming the Q0 is dead simple.</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:3728px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Jq7ePodRxeyrgX9Xxr6Cug" name="keychron-launcher-q0-backlighting" alt="A screenshot showing how you can customize the Keychron Q0 Mini 8K Action Key's RGB lighting using the Keychron Launcher" src="https://cdn.mos.cms.futurecdn.net/Jq7ePodRxeyrgX9Xxr6Cug-1920-80.jpg" mos="" align="middle" fullscreen="" width="3728" height="2097" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Besides remapping the Q0’s single key, you can also customize its RGB lighting in the Keychron Launcher. There are several colors and lighting effects to pick from, but I ended up going with something as close to turquoise as I could find to match my keyboard’s Esc and Enter accent keys. </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="camNHWqjrcPmhHhoyDbCHk" name="Keychron-Q0--3" alt="The Keychron Q0 Mini 8K Action Key next to a split mechanical keyboard on a desk" src="https://cdn.mos.cms.futurecdn.net/camNHWqjrcPmhHhoyDbCHk-1920-80.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>During the time I’ve had the Q0 on my desk, slamming down its extra-large key has helped break up the monotonous parts of my day. </p><p>For instance, when I’m moving photos off my camera and need an extra File Explorer window to drag the keepers to, I just press it down and one appears instantly. Likewise, if one of my coworkers needs a file like a deal badge or even a Photoshop template I previously downloaded, the Q0 has made quickly pulling those up a lot more fun. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-ev1vyX"></div>                            </div>                            <script src="https://kwizly.com/embed/ev1vyX.js" async></script><h2 id="from-micro-to-a-macro-macropad">From micro to a macro macropad</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1953px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="qcYwDQ7GmY58dR9qAKSH6" name="macropads-split-3" alt="A composite shot showing the Keychron Q0 Mini 8K Action Key next to a custom, two-key macropad" src="https://cdn.mos.cms.futurecdn.net/qcYwDQ7GmY58dR9qAKSH6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1953" height="1099" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Even though I had known about macropads for years, I actually built my first one. You see, back then, I had a lot more time on my hands and had previously built my own custom mechanical keyboard. This was back before<a href="https://www.tomsguide.com/computing/peripherals/7-things-to-know-when-choosing-your-first-mechanical-keyboard"> hot-swappable switches</a> were a thing, so I had to solder each switch individually. While that took a bit of work, I learned a lot going through the process. </p><p>When I saw a two-key macropad kit with a hot-swappable PCB pop up online, I figured it was worth trying out, even if I didn’t have a dedicated use case for it in mind at the time.</p><p>At first, I thought about taking the easy way out and mapping one key to copy and the other to paste. However, since I work with large blocks of text while writing and editing every day, I already had Ctrl + C and Ctrl + V mapped to my trusty <a href="https://www.tomsguide.com/reference/what-is-a-trackball-mouse">trackball mouse</a>. There was one thing my PC — or should I say my monitor’s built-in speakers — didn’t have at the time: a volume knob. So that’s what I used my <a href="https://www.tomsguide.com/opinion/this-dollar20-accessory-solved-one-of-my-biggest-problems-and-changed-how-i-control-my-pc">tiny, two-key macropad</a> for.</p><p>Besides their excellent build quality and value, one of my favorite things about Keychron’s keyboards is that many come with a built-in volume knob as standard. I’ve been using a keyboard with one such knob for the past few years, so my old macropad has gone to the box of decommissioned gadgets in my closet.</p><p>Going from two keys to one might seem like a downgrade at first. However, thanks to Keychron’s excellent launcher and everything I’ve learned in the years since, it’s been surprisingly useful having it on my desk. </p><p>Is it unnecessary though? Sure, but that defeats the purpose. A fun little — or should I say big — gadget like the Q0 was just what my <a href="https://www.tomsguide.com/computing/i-screenmaxxed-my-desk-setup-by-going-from-three-to-five-displays-but-they-arent-all-monitors">desk setup</a> was missing. Instead of being utilitarian like the rest of my accessories, it adds a touch of personal flair, while slamming it down brings a bit of fun my workspace was sorely lacking.</p><p>Now, I don’t know yet if the Q0 will remain a permanent fixture on my desk, but for now, I’ll gladly slam it down every time I’m looking for a local file.</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/computing/keyboards/i-tried-the-new-keychron-q1-ultra-8k-and-this-hefty-full-featured-keyboard-is-a-game-changer-for-my-workflow">I tested the Keychron Q1 Ultra 8K and this full-metal body mechanical keyboard is a game changer for my workflow</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/i-thought-a-vertical-mouse-was-no-match-for-my-trackball-until-i-tried-one-and-now-im-never-going-back">I finally tried a vertical mouse and it was the ergonomic upgrade my desk setup was missing</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-tried-this-mechanical-keyboard-with-a-built-in-pointing-stick-and-it-almost-convinced-me-to-ditch-my-mouse">I tried this mechanical keyboard with a built-in pointing stick and it almost convinced me to ditch my mouse</a></li></ul><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=mechanical+keyboards&brands=Epomaker%2CKeychron%2CNuPhy%2CRazer&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, .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/computing/peripherals/i-added-this-one-key-macropad-to-my-desk-setup-and-slamming-it-down-is-the-perfect-way-to-break-up-my-workday</link>
                                                                            <description>
                            <![CDATA[ Keychron's Q0 Mini 8K Action Key may only have a single, supersized mechanical key switch, but it's built like a premium keyboard. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">t9jSQFNg4VroRwL9bHj5BN</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/64S2QcxrEVxq56TVAgCLvn-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sun, 19 Jul 2026 10:30:00 +0000</pubDate>                                                                                                                                <updated>Tue, 21 Jul 2026 19:04:56 +0000</updated>
                                                                                                                                            <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                <author><![CDATA[ anthony.spadafora@futurenet.com (Anthony Spadafora) ]]></author>                    <dc:creator><![CDATA[ Anthony Spadafora ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/z73LEoj7FkUjNG85GcWHtH-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Anthony Spadafora is the managing editor for security and home office furniture at Tom’s Guide where he covers everything from data breaches and malware to password managers and the best way to cover your whole home or business with a strong Wi-Fi signal.&lt;/p&gt;&lt;p&gt;Before joining the team, he spent three years covering cybersecurity and B2B tech for ITProPortal while living in South Korea. After moving back to the US. Anthony joined the TechRadar Pro team where he covered these topics along with VPNs, web hosting, online collaboration software and video conferencing for four years. Anthony also has his ears to the ground and is on the lookout for the next major cyberattack or data breach.&lt;/p&gt;&lt;p&gt;Based in Houston, Texas, Anthony also handles VPN testing for both Tom’s Guide and TechRadar. As someone who has worked from home exclusively since 2018, he has reviewed dozens of standing desks as well as office chairs and has taken a closer look at other essential remote working accessories. As part of these reviews, Anthony frequently builds intricate desk setups which is why he’s such a big advocate for cable management and keeping things organized. When he’s not writing, he can be found tinkering with PCs and game consoles, managing cables and making upgrades to his smart home.&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/64S2QcxrEVxq56TVAgCLvn-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A person using the Keychron Q0 Mini 8K Action Key at their desk]]></media:description>                                                            <media:text><![CDATA[A person using the Keychron Q0 Mini 8K Action Key at their desk]]></media:text>
                                <media:title type="plain"><![CDATA[A person using the Keychron Q0 Mini 8K Action Key at their desk]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/64S2QcxrEVxq56TVAgCLvn-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>There comes a time when doing things easier and faster becomes your sole motivation and the driving force behind learning new things on your computer. </p><p>Whether that’s doing complex Photoshop edits in fewer clicks or mastering even the most obscure <a href="https://www.tomsguide.com/computing/the-internet-is-sharing-its-favorite-keyboard-shortcuts-and-i-cant-believe-im-just-learning-some-of-these">keyboard shortcuts</a>, once you fall down that rabbit hole, it seems like you’ll never reach the bottom.</p><p>Like with most people, this started small for me. A keyboard shortcut here and a keyboard shortcut there, until I was outright remapping them to buttons on my mouse. Then I took things a step further and did so <a href="https://www.tomsguide.com/opinion/i-used-this-tiny-game-controller-as-a-productivity-tool-for-work-heres-what-happened">with a tiny controller</a> from 8BitDo and <a href="https://www.tomsguide.com/opinion/i-tried-using-a-foot-pedal-to-increase-my-productivity-at-work-heres-what-happened">even a foot pedal</a> connected to my PC. However, there was an easier way: macropads.</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/njtcgcqZWXk" allowfullscreen></iframe></div></div><p>Designed to work alongside your keyboard and mouse, macropads are fully customizable accessories where you can map whatever you want to each key. From simple things like copy and paste to full-on commands, the possibilities are truly endless. However, what if you only had a single, comically large key to work with?</p><p>Well, that’s exactly the position I found myself in when I decided to add Keychron’s Q0 Mini 8K Action Key to my desk setup. While a traditional macropad certainly would have been more useful, reaching over to slam this one down throughout my workday has been a whole lot more fun. </p><div class="product"><a data-dimension112="714be9ba-820b-11f1-8e2c-4f2cd85e9bbb" data-action="Deal Block" data-label="Q0 Mini 8K Action Key" data-dimension48="Q0 Mini 8K Action Key" data-dimension25="$65" href="https://www.keychron.com/products/keychron-q0-mini-8k-action-key" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1214px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="kPxKMVufz87tSrXaZa6dbT" name="Q0 Mini 8K Action Key" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/kPxKMVufz87tSrXaZa6dbT-1920-80.webp" mos="" align="middle" fullscreen="" width="1214" height="1214" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.keychron.com/products/keychron-q0-mini-8k-action-key" target="_blank" rel="nofollow" data-dimension112="714be9ba-820b-11f1-8e2c-4f2cd85e9bbb" data-action="Deal Block" data-label="Q0 Mini 8K Action Key" data-dimension48="Q0 Mini 8K Action Key" data-dimension25="$65">Q0 Mini 8K Action Key: $65</a></strong><br>This comically oversized, single-key macropad features a blazing-fast 8K polling rate for near-instant inputs and an ultra-durable machined aluminum base. You can completely customize its single action key or record complex multi-key macros using the browser-based Keychron Launcher too. In addition to a custom, giant 64x Outemu mechanical switch under the hood, the Q0 also features RGB lighting you can tweak to your liking.<a class="view-deal button" href="https://www.keychron.com/products/keychron-q0-mini-8k-action-key" target="_blank" rel="nofollow" data-dimension112="714be9ba-820b-11f1-8e2c-4f2cd85e9bbb" data-action="Deal Block" data-label="Q0 Mini 8K Action Key" data-dimension48="Q0 Mini 8K Action Key" data-dimension25="$65">View Deal</a></p></div><h2 id="not-your-typical-keychron-keyboard">Not your typical Keychron keyboard</h2><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="KB9umEgX6x6g7qQshK87EH" name="Keychron-Q0--7" alt="The Keychron Q0 Mini 8K Action Key unboxed on a table" src="https://cdn.mos.cms.futurecdn.net/KB9umEgX6x6g7qQshK87EH-1920-80.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>From a <a href="https://www.tomsguide.com/computing/peripherals/i-spent-just-dollar30-on-a-mechanical-keyboard-to-prove-you-dont-need-to-waste-your-money">super budget-friendly wired board</a> to a <a href="https://www.tomsguide.com/computing/keyboards/i-used-keychrons-new-split-mechanical-keyboard-for-a-week-and-its-tether-free-design-is-a-complete-game-changer">split ergonomic one</a>, I’ve tried out a lot of Keychron’s keyboards over the past few years. In fact, I’d go so far as to say that the company makes some of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> you can get today without breaking the bank. Keychron’s Q0 Mini 8K Action Key may only have a single key switch, but unboxing it still gave me the familiar experience I’ve come to expect from the brand.</p><p>In the box, you get the Q0 Mini 8K Action Key itself along with a braided USB-C cable, and just in case you don’t have any free USB-C ports, you also get a USB-A adapter. For a device that could have easily been an <a href="https://www.tomsguide.com/news/live/april-fools-day-2026-live-best-jokes-pranks">April Fools' joke</a>, everything feels a whole lot more premium than it has any right to be.</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="yPFtHUboHAH7uHuXBJ6w3L" name="Keychron-Q0--6" alt="The USB-C port at the back of the Keychron Q0 Mini 8K Action Key" src="https://cdn.mos.cms.futurecdn.net/yPFtHUboHAH7uHuXBJ6w3L-1920-80.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 first thing that stuck out to me about the Q0 — besides the fact that it’s massive at 3.65 x 3.25 x 3.25 inches — is its heft. This thing isn’t going anywhere on your desk thanks to its machined aluminum base. Even with its oversized keycap, the Q0 weighs nearly a pound on its own.</p><p>On the backside, there’s a single, recessed USB-C port and that’s all you get. While I would have loved for it to have Bluetooth or wireless 2.4 GHz connectivity, Keychron went overboard with its wired connection. A single key like this doesn’t need to be that fast, but the company went ahead and gave it a polling rate of 8,000 Hz anyway, which I consider overkill in the best possible way.</p><p>I have to admit, I didn’t read the instructions as I was just too excited to try out this single-key macropad for giants. But before I took it over to my desk and plugged it in, there was one more thing I wanted to try, and boy am I glad I did.</p><h2 id="one-key-to-rule-them-all">One key to rule them all</h2><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="4ZiE2aiR3DBhmdG8mbCsiN" name="Keychron-Q0--5" alt="The Keychron Q0 Mini 8K Action Key with its keycap removed" src="https://cdn.mos.cms.futurecdn.net/4ZiE2aiR3DBhmdG8mbCsiN-1920-80.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>Unlike those cheap membrane keyboards you find in offices or libraries, one of my favorite things about mechanical keyboards is that they’re just as easy to clean as they are to customize. And that all comes down to the fact that with a bit of back and forth, their keycaps pop right off.</p><p>Now if the Q0’s keycap had been permanently attached to the switch underneath it, I wouldn’t have knocked Keychron for it in the slightest. Much to my surprise though, when I gave its double-shot PBT keycap — with a rocket ship emblazoned on top — a bit of a wiggle, it popped right off. Oh, and if you don’t like rockets, Keychron has the <a href="https://www.keychron.com/pages/multi-view-drawing-keychron-q0-mini-8k-action-key" target="_blank">switch and keycap spec drawing</a> on its site so you can make your own with one of the <a href="https://www.tomsguide.com/us/best-3d-printers,review-2236.html">best 3D printers</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="hXNLRhSjXnRbr5GiVuZmYS" name="Keychron-Q0--4" alt="A closeup shot of the key switch on the Keychron Q0 Mini 8K Action Key" src="https://cdn.mos.cms.futurecdn.net/hXNLRhSjXnRbr5GiVuZmYS-1920-80.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>Not only did this let me see the inner workings of this extra-large 3 x 2.5-inch keycap, but I also got a closer look at the mechanical key switch underneath it. By working closely with Outemu, Keychron created a scaled-up version of a traditional key switch that’s actually 64 times larger. It has your standard MX-style cross stem and a south-facing LED for the RGB goodness often associated with mechanical keyboards.</p><p>I’ve been using a mechanical keyboard for over a decade now, but I wish there was something like this when I first got into the hobby. It’s one thing to see a diagram of how all the various types of <a href="https://www.tomsguide.com/computing/peripherals/i-made-a-huge-mistake-with-my-new-mechanical-keyboard-but-this-feature-saved-the-day">mechanical key switches</a> work and it’s another thing to hold an oversized one in your hands.</p><p>With my curiosity piqued, it was time to plug the Q0 in and see if a massive macropad was as fun to use as it was to hold in my hands. Spoiler alert: it definitely is.</p><h2 id="bottoming-out-never-felt-this-good">Bottoming out never felt this 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:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uJkyhhipskP3QXg5KeFQ2Y" name="Keychron-Q0--2" alt="The Keychron Q0 Mini 8K Action Key on the edge of a desk" src="https://cdn.mos.cms.futurecdn.net/uJkyhhipskP3QXg5KeFQ2Y-1920-80.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>Next to the mechanical keyboard on my desk, the Q0 looks almost absurd. It’s about the size of a coaster, and unlike the rest of the peripherals in my current desk setup, it’s the only wired one. </p><p>Still, being able to reach over and slam it down to pull off pretty much anything I can think of is absolutely worth it. And for the keen-eyed among you, yes, the keyboard I’m currently using — the <a href="https://www.tomsguide.com/computing/keyboards/i-used-keychrons-new-split-mechanical-keyboard-for-a-week-and-its-tether-free-design-is-a-complete-game-changer">Keychron Q11 Ultra</a> — already has five programmable macro keys.</p><p>With the Q0 plugged into the <a href="https://www.tomsguide.com/computing/mini-pcs/geekom-a9-max-review">Geekom A9 Max</a> mini PC under my monitor, I pressed it and ... nothing happened. Yeah, I was a little surprised at first, too. I figured the Q0’s massive key would be mapped to Esc or Enter out of the box, but that wasn’t the case. This is a device for tinkerers and mechanical keyboard enthusiasts after all, which is why Keychron likely decided to ship it unmapped.</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:3725px;"><p class="vanilla-image-block" style="padding-top:56.24%;"><img id="vd84JZZCNUnugLNxNougsb" name="keychron-launcher-q0-custom-keys" alt="A screenshot showing how you can customize the Keychron Q0 Mini 8K Action Key using the Keychron Launcher" src="https://cdn.mos.cms.futurecdn.net/vd84JZZCNUnugLNxNougsb-1920-80.jpg" mos="" align="middle" fullscreen="" width="3725" height="2095" attribution="" endorsement="" class="inline"></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 with the rest of Keychron’s more recent keyboards and mice, complete customization is just a browser tab away. So I loaded up the <a href="https://launcher.keychron.com/" target="_blank">Keychron Launcher</a> in Chrome and the Q0 was instantly recognized. Normally in this browser-based launcher, you’re treated to a whole keyboard’s worth of keys you can tweak and customize to your liking. Here though, as you’d expect, there’s only one key to work with.</p><p>Sure, you could program it to Esc, Enter, or even the Windows key, but where’s the fun in that? Likewise, since it’s compatible with Mac (and Linux), you could have it work as a giant Command key too. However, I immediately went to the Special Keys option in the Keychron Launcher and saw what was available for each platform.</p><p>If I wanted to lose my work every time my 4-year-old came to my desk, I could have easily programmed the Q0 to shut down my computer, but I’m not a glutton for punishment. Alternatively, it would also work quite well as a massive mute/unmute button for video calls. </p><p>Remember what I said before about fewer clicks? Well, opening File Explorer on Windows is something I do quite often, and who wants to mouse down to their taskbar or open the Start Menu? After trying out a few different customizations with the Q0, I settled on having it open a new File Explorer window.</p><p>At the same time, you can take things even further by recording your own macros — think complex multi-key commands — and programming them to the Q0. I wanted to keep things simple though, so I’ve mainly stuck to File Explorer for now. Still, it’s great knowing that when I do think of a unique new use case or come up with a clever macro, reprogramming the Q0 is dead simple.</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:3728px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Jq7ePodRxeyrgX9Xxr6Cug" name="keychron-launcher-q0-backlighting" alt="A screenshot showing how you can customize the Keychron Q0 Mini 8K Action Key's RGB lighting using the Keychron Launcher" src="https://cdn.mos.cms.futurecdn.net/Jq7ePodRxeyrgX9Xxr6Cug-1920-80.jpg" mos="" align="middle" fullscreen="" width="3728" height="2097" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Besides remapping the Q0’s single key, you can also customize its RGB lighting in the Keychron Launcher. There are several colors and lighting effects to pick from, but I ended up going with something as close to turquoise as I could find to match my keyboard’s Esc and Enter accent keys. </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="camNHWqjrcPmhHhoyDbCHk" name="Keychron-Q0--3" alt="The Keychron Q0 Mini 8K Action Key next to a split mechanical keyboard on a desk" src="https://cdn.mos.cms.futurecdn.net/camNHWqjrcPmhHhoyDbCHk-1920-80.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>During the time I’ve had the Q0 on my desk, slamming down its extra-large key has helped break up the monotonous parts of my day. </p><p>For instance, when I’m moving photos off my camera and need an extra File Explorer window to drag the keepers to, I just press it down and one appears instantly. Likewise, if one of my coworkers needs a file like a deal badge or even a Photoshop template I previously downloaded, the Q0 has made quickly pulling those up a lot more fun. </p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-ev1vyX"></div>                            </div>                            <script src="https://kwizly.com/embed/ev1vyX.js" async></script><h2 id="from-micro-to-a-macro-macropad">From micro to a macro macropad</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1953px;"><p class="vanilla-image-block" style="padding-top:56.27%;"><img id="qcYwDQ7GmY58dR9qAKSH6" name="macropads-split-3" alt="A composite shot showing the Keychron Q0 Mini 8K Action Key next to a custom, two-key macropad" src="https://cdn.mos.cms.futurecdn.net/qcYwDQ7GmY58dR9qAKSH6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1953" height="1099" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Even though I had known about macropads for years, I actually built my first one. You see, back then, I had a lot more time on my hands and had previously built my own custom mechanical keyboard. This was back before<a href="https://www.tomsguide.com/computing/peripherals/7-things-to-know-when-choosing-your-first-mechanical-keyboard"> hot-swappable switches</a> were a thing, so I had to solder each switch individually. While that took a bit of work, I learned a lot going through the process. </p><p>When I saw a two-key macropad kit with a hot-swappable PCB pop up online, I figured it was worth trying out, even if I didn’t have a dedicated use case for it in mind at the time.</p><p>At first, I thought about taking the easy way out and mapping one key to copy and the other to paste. However, since I work with large blocks of text while writing and editing every day, I already had Ctrl + C and Ctrl + V mapped to my trusty <a href="https://www.tomsguide.com/reference/what-is-a-trackball-mouse">trackball mouse</a>. There was one thing my PC — or should I say my monitor’s built-in speakers — didn’t have at the time: a volume knob. So that’s what I used my <a href="https://www.tomsguide.com/opinion/this-dollar20-accessory-solved-one-of-my-biggest-problems-and-changed-how-i-control-my-pc">tiny, two-key macropad</a> for.</p><p>Besides their excellent build quality and value, one of my favorite things about Keychron’s keyboards is that many come with a built-in volume knob as standard. I’ve been using a keyboard with one such knob for the past few years, so my old macropad has gone to the box of decommissioned gadgets in my closet.</p><p>Going from two keys to one might seem like a downgrade at first. However, thanks to Keychron’s excellent launcher and everything I’ve learned in the years since, it’s been surprisingly useful having it on my desk. </p><p>Is it unnecessary though? Sure, but that defeats the purpose. A fun little — or should I say big — gadget like the Q0 was just what my <a href="https://www.tomsguide.com/computing/i-screenmaxxed-my-desk-setup-by-going-from-three-to-five-displays-but-they-arent-all-monitors">desk setup</a> was missing. Instead of being utilitarian like the rest of my accessories, it adds a touch of personal flair, while slamming it down brings a bit of fun my workspace was sorely lacking.</p><p>Now, I don’t know yet if the Q0 will remain a permanent fixture on my desk, but for now, I’ll gladly slam it down every time I’m looking for a local file.</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/computing/keyboards/i-tried-the-new-keychron-q1-ultra-8k-and-this-hefty-full-featured-keyboard-is-a-game-changer-for-my-workflow">I tested the Keychron Q1 Ultra 8K and this full-metal body mechanical keyboard is a game changer for my workflow</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/i-thought-a-vertical-mouse-was-no-match-for-my-trackball-until-i-tried-one-and-now-im-never-going-back">I finally tried a vertical mouse and it was the ergonomic upgrade my desk setup was missing</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-tried-this-mechanical-keyboard-with-a-built-in-pointing-stick-and-it-almost-convinced-me-to-ditch-my-mouse">I tried this mechanical keyboard with a built-in pointing stick and it almost convinced me to ditch my mouse</a></li></ul><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=mechanical+keyboards&brands=Epomaker%2CKeychron%2CNuPhy%2CRazer&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, .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[ The Corsair Sabre V2 Pro is a mighty impressive mouse with 8,000Hz polling and 33,000 DPI — here’s why it’s great for FPS and esports ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The Corsair Sabre V2 Pro is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a> you can buy. Lightweight yet sturdy, the Sabre V2 Pro is built well and is comfortable for long gaming sessions. It’s biggest selling point is its 8,000Hz polling rate over wired and wireless connections, and 33,000 DPI — more than plenty to give you a competitive advantage.</p><p>Alongside all this, you also get web-based companion software that makes customization a walk in the park. The best part is that the Sabre V2 Pro does all this without breaking the bank, making it a great alternative to expensive flagships from the likes of Razer. But the mouse isn’t perfect, as battery life isn’t the strongest, and there’s no dedicated DPI button.</p><p>For the complete breakdown, read my full Corsair Sabre V2 Pro review.</p><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-specs"><span>Corsair Sabre V2 Pro 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/Corsair-Sabre-Ultralight-Wireless-Gaming/dp/B0FKJ2J2R8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Corsair-SABRE-Ultralight-Wireless-Gaming/dp/B0FPDBK2M6/" target="_blank" rel="nofollow">£89</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>33,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>5</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.69 x 2.46 x 1.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.26oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 2.4GHz dongle</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>70 hours (1,000Hz) | 16 hours (8,000Hz)</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-cheat-sheet"><span>Corsair Sabre V2 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>An ultralight gaming mouse with 33,000 DPI and 8,000Hz polling</li><li><strong>Who is it for? </strong>Competitive gamers and FPS players</li><li><strong>How much does it cost? </strong>The Corsair Sabre V2 Pro is available for <a href="https://www.amazon.com/Corsair-Sabre-Ultralight-Wireless-Gaming/dp/B0FKJ2J2R8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Corsair-SABRE-Ultralight-Wireless-Gaming/dp/B0FPDBK2M6/" target="_blank" rel="nofollow">£89</a></li><li><strong>What do we like?</strong> The ultralight yet sturdy design, excellent gaming performance, web-based software, and competitive price</li><li><strong>What don’t we like? </strong>The poor battery life and absence of a dedicated DPI switch</li></ul><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-the-ups"><span>Corsair Sabre V2 Pro review: The ups</span></h2><p>The Corsair Sabre V2 Pro is ultralight and comfortable, and it boasts powerful gaming performance across a range of genres.</p><h2 id="ultralight-yet-sturdy">Ultralight yet sturdy</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zPMdLWTFu6ES7sVKA4vZwN" name="corsair_Sabre_V2_ 1.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/zPMdLWTFu6ES7sVKA4vZwN-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Ultralight mice are all the rage today. They require less physical force to move and stop, which increases agility and results in faster cursor speeds, and even proves less fatiguing during long gaming sessions. As a competitive gaming mouse, the Corsair Sabre V2 Pro is extremely light — one of the lightest mice I’ve tested, in fact. It weighs just 1.26oz, so it’s lighter than the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini V2 8K</a> ($69, 1.94oz), the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m3-mini-4k-metal-review">Keychron M3 Mini 4K Metal</a> ($99, 2.25oz) and the phenomenal <a href="https://www.tomsguide.com/computing/mice/razer-viper-v4-pro-review">Razer Viper V4 Pro</a> ($159, 1.72oz).</p><p>What I love about the Sabre V2 Pro is that even though it’s as light as a feather, it’s still extremely sturdy, and it feels substantial under my palm. It’s made of high-quality plastic that looks and feels premium, and its skates effortlessly glide across most surfaces. Corsair happily includes spare skates in the box just in case the originals wear down. Also included in the box are pieces of anti-slip grip tape, which can be stuck to the mouse for additional grip.</p><p>Speaking of, the Sabre V2 Pro on its own feels very grippy. I didn’t feel like my fingers were slipping at any point during testing. The mouse measures 4.69 x 2.46 x 1.5 inches and I found it suitable for my medium-sized hands — although if you’ve got big hands, you may find the mouse a tad small (unless you’re a claw gripper). A chunkier mouse like the <a href="https://www.tomsguide.com/computing/peripherals/fnatic-x-lamzu-maya-8k-maya-x-8k-mouse-review">Fnatic x Lamzu Maya X 8K</a> ($119) or the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69) may be more suitable, if that’s the case.</p><h2 id="excellent-gaming-performance">Excellent gaming 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="guEdfrtduHNHXEz4cnVX2L" name="corsair_Sabre_V2_ 2.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/guEdfrtduHNHXEz4cnVX2L-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="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">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>The Corsair Sabre V2 Pro is a gaming mouse, and a good one at that. Packing a maximum of 33,000 DPI and 8,000Hz polling over wired and 2.4GHz wireless connections, the Sabre V2 Pro is an excellent choice for esports and FPS gamers. The polling rate can be dropped down to 1,000Hz using the mouse’s companion software (more on that shortly) if battery life is a concern (more on that later, too).</p><p>As I always do when testing gaming mice, I played a variety of games including Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> on our testing lab’s gaming rig — and on the <a href="https://www.tomsguide.com/computing/gaming-laptops/msi-crosshair-16-hx-e14w-review">MSI Crosshair 16 HX E14W</a> laptop too, which I was testing alongside the mouse. I kicked things off with Counter-Strike 2. Thanks to 8,000Hz polling, I didn’t notice any latency issues or input lag, and all my actions were instantaneously registered.</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="iL4C24YwykcWXRv3JX6ujK" name="corsair_Sabre_V2_ 6.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/iL4C24YwykcWXRv3JX6ujK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 33,000 DPI is the real star here. Being able to swap between a low DPI of around 400 when aiming my sniper rifle at a target in the distance, and a high DPI of 8,000 or more when engaging in close-quarters combat gave me a competitive advantage. With the DPI set to 400, I was able to slow down my movements which helped me accurately aim at my target and execute headshots, especially when stealthily infiltrating enemy camps in Cyberpunk 2077.</p><p>Alongside rapid polling and DPI, you also get <a href="https://www.tomsguide.com/computing/what-is-angle-snapping">angle snapping</a> which can be enabled via Corsair Web Hub. In a nutshell, angle snapping smooths out mouse movements to ensure a straight line, thereby eliminating any jitters caused by hand shake. This helps you keep your cursor at head-level at all times which is useful in competitive games like Counter-Strike 2. I managed to take out a row of players as my cursor movements were stabilized and my crosshairs moved in a straighter line than they would if I didn’t have angle snapping toggled.</p><h2 id="web-based-companion-software">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/bw4Yvq2Jas7VpmWAuosiDe-1920-80.jpg" alt="Corsair Web Hub screenshot" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/jkDFi4gtpxcTKs5DWusiDe-1920-80.jpg" alt="Corsair Web Hub screenshot" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/duooKUk4H5Pgjajj7mTd9e-1920-80.jpg" alt="Corsair Web Hub screenshot" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure></figure><p>I love it when gadgets are accompanied by web-based software for customization as it negates the need for downloading proprietary apps or bloatware. The Corsair Sabre V2 Pro is compatible with <a href="https://www.corsair.com/web-hub/" target="_blank" rel="nofollow">Corsair Web Hub</a> which enables you to remap buttons, assign different DPI values to the five stages, adjust polling rate and lift height, and enable angle snapping. </p><p>Corsair Web Hub sports a clean interface and user-friendly design, with no unnecessary or intrusive ads or pop-ups. All you need to do is plug in your mouse using the included USB-C cable and launch Corsair Web Hub in Google Chrome, Edge, or other browser, and you’re good to go.</p><h2 id="competitively-priced-3">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AakGuvFKFixX82qXSwMPtM" name="corsair_Sabre_V2_ 3.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/AakGuvFKFixX82qXSwMPtM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 don’t need to spend hundreds on a competent, competitive mouse, and the Corsair Sabre V2 Pro proves it. The mouse has a retail price of <a href="https://www.amazon.com/Corsair-Sabre-Ultralight-Wireless-Gaming/dp/B0FKJ2J2R8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Corsair-SABRE-Ultralight-Wireless-Gaming/dp/B0FPDBK2M6/" target="_blank" rel="nofollow">£89 at Amazon</a>, and I’ve seen it drop to $62 / £59 in the past, so it’s worth keeping an eye out during sales events. Even at its retail price, the Sabre V2 Pro is cheaper than other esports mice, like the Razer Viper V4 Pro ($159) and the <a href="https://www.tomsguide.com/computing/mice/logitech-g-pro-x2-superstrike-review">Logitech G Pro X2 Superstrike</a> ($179).</p><p>If you think the Sabre V2 Pro is out of your budget, both the Keychron M3 Mini V2 8K and the Keychron M6 8K cost $69 each. But Corsair’s offering is worth the extra spend if you want 33,000 DPI packed into an ultralight body.</p><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-the-downs"><span>Corsair Sabre V2 Pro review: The downs</span></h2><p>There isn’t a lot wrong with the Corsair Sabre V2 Pro but it’s worth noting that when polling at 8,000Hz, battery life doesn’t last as long, and there’s no dedicated DPI button.</p><h2 id="short-battery-life">Short battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Lj93fRzDyzW7UdyHNfJndN" name="corsair_Sabre_V2_ 5.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/Lj93fRzDyzW7UdyHNfJndN-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Sabre V2 Pro isn’t winning any awards when it comes to battery life, which can best be described as mediocre. Over 1,000Hz polling, you can get up to 70 hours between charges, which doesn’t sound too bad. But switch to 8,000Hz polling, and battery life drops to a measly 16 hours. After eight or so hours of playing, the battery had dropped to 45%, on course to die after the stated time.</p><p>This isn’t unexpected as 8KHz polling is a drain on both your system and the device, and you’ll find this with the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a>, too. If you want longer runtime, I’d recommend the Keychron M6 8K (35 hours) or the Keychron M3 Mini V2 8K (40 hours). The Razer Viper V4 Pro can also last up to 40 hours between charges but of course, it costs a lot more.</p><h2 id="no-onboard-dpi-switch">No onboard DPI switch</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rqBGJAAT55Z8t4B7UAQzeL" name="corsair_Sabre_V2_ 4.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/rqBGJAAT55Z8t4B7UAQzeL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 gripe with the Corsair Sabre V2 Pro is its lack of an onboard DPI switch. I’ve tested my fair share of mice — I always appreciate having a button on the mouse itself to quickly swap between different DPI values as in-game situations change. Unfortunately, doing so on the fly on the Sabre V2 Pro isn’t possible. To change the DPI, you’ll need to either launch the web-based companion software every time, or long-press the right-click and back button (located on the mouse’s left edge) for three seconds.</p><p>While the latter is simple enough to do, it’s time consuming and unnecessary, in my opinion, and it could have been made easier by simply pressing a dedicated DPI button. If you’re competing in an esports tournament, or playing competitively otherwise, you wouldn’t want to lose precious seconds, would you? This, on its own, stops me from outright recommending the Sabre V2 Pro.</p><p>If you want a gaming mouse that makes it easier to swap between DPI stages, I’d recommend the Keychron M6 8K, the Keychron M3 Mini V2 8K or the Fnatic x Lamzu Maya X 8K. The Razer Viper V4 Pro would be my personal top choice, but it costs nearly $60 more than the Sabre V2 Pro.</p><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-verdict"><span>Corsair Sabre V2 Pro 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="PBBiVrkVEvg8Q4dSQi5c4M" name="corsair_Sabre_V2_ 7.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/PBBiVrkVEvg8Q4dSQi5c4M-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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’ve got a budget of $100 and want a powerful mouse, the Corsair Sabre V2 Pro is the answer. The mouse is ridiculously light and comfortable, and it glides effortlessly across surfaces. It’s built very well, and it’s accompanied by user-friendly web-based companion software. With 33,000 DPI and 8,000Hz polling at your disposal, the Sabre V2 Pro gives you a competitive edge in FPS and esports.</p><p>Few things in life are perfect, though, and the Sabre V2 Pro isn’t one of them. Its battery life isn’t the strongest, lasting just 16 hours when polling at 8,000Hz, and there’s no dedicated DPI button, so you have to fiddle with click combinations to change it on the fly.</p><p>If neither of those feel like dealbreakers, then the Sabre V2 Pro could be perfect for you. It’s sleek, comfortable and powerful, and it makes even the worst Counter-Strike 2 player (like myself) look half competent.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/mice/corsair-sabre-v2-pro-review</link>
                                                                            <description>
                            <![CDATA[ The Corsair Sabre V2 Pro is a great esports and FPS mouse with 33,000 DPI and 8,000Hz polling, and it's lightweight and comfortable. But it has a couple flaws. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">3gv9MUdPVQHU99fdxKLaNE</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/6jbT8m7hjErbrpURbuid8N-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 17 Jul 2026 17:59:56 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Mice]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/6jbT8m7hjErbrpURbuid8N-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Corsair Sabre V2 Pro wireless gaming mouse]]></media:description>                                                            <media:text><![CDATA[A black Corsair Sabre V2 Pro wireless gaming mouse]]></media:text>
                                <media:title type="plain"><![CDATA[A black Corsair Sabre V2 Pro wireless gaming mouse]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/6jbT8m7hjErbrpURbuid8N-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The Corsair Sabre V2 Pro is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a> you can buy. Lightweight yet sturdy, the Sabre V2 Pro is built well and is comfortable for long gaming sessions. It’s biggest selling point is its 8,000Hz polling rate over wired and wireless connections, and 33,000 DPI — more than plenty to give you a competitive advantage.</p><p>Alongside all this, you also get web-based companion software that makes customization a walk in the park. The best part is that the Sabre V2 Pro does all this without breaking the bank, making it a great alternative to expensive flagships from the likes of Razer. But the mouse isn’t perfect, as battery life isn’t the strongest, and there’s no dedicated DPI button.</p><p>For the complete breakdown, read my full Corsair Sabre V2 Pro review.</p><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-specs"><span>Corsair Sabre V2 Pro 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/Corsair-Sabre-Ultralight-Wireless-Gaming/dp/B0FKJ2J2R8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Corsair-SABRE-Ultralight-Wireless-Gaming/dp/B0FPDBK2M6/" target="_blank" rel="nofollow">£89</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>33,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>5</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.69 x 2.46 x 1.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.26oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 2.4GHz dongle</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>70 hours (1,000Hz) | 16 hours (8,000Hz)</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-cheat-sheet"><span>Corsair Sabre V2 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>An ultralight gaming mouse with 33,000 DPI and 8,000Hz polling</li><li><strong>Who is it for? </strong>Competitive gamers and FPS players</li><li><strong>How much does it cost? </strong>The Corsair Sabre V2 Pro is available for <a href="https://www.amazon.com/Corsair-Sabre-Ultralight-Wireless-Gaming/dp/B0FKJ2J2R8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Corsair-SABRE-Ultralight-Wireless-Gaming/dp/B0FPDBK2M6/" target="_blank" rel="nofollow">£89</a></li><li><strong>What do we like?</strong> The ultralight yet sturdy design, excellent gaming performance, web-based software, and competitive price</li><li><strong>What don’t we like? </strong>The poor battery life and absence of a dedicated DPI switch</li></ul><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-the-ups"><span>Corsair Sabre V2 Pro review: The ups</span></h2><p>The Corsair Sabre V2 Pro is ultralight and comfortable, and it boasts powerful gaming performance across a range of genres.</p><h2 id="ultralight-yet-sturdy">Ultralight yet sturdy</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zPMdLWTFu6ES7sVKA4vZwN" name="corsair_Sabre_V2_ 1.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/zPMdLWTFu6ES7sVKA4vZwN-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Ultralight mice are all the rage today. They require less physical force to move and stop, which increases agility and results in faster cursor speeds, and even proves less fatiguing during long gaming sessions. As a competitive gaming mouse, the Corsair Sabre V2 Pro is extremely light — one of the lightest mice I’ve tested, in fact. It weighs just 1.26oz, so it’s lighter than the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini V2 8K</a> ($69, 1.94oz), the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m3-mini-4k-metal-review">Keychron M3 Mini 4K Metal</a> ($99, 2.25oz) and the phenomenal <a href="https://www.tomsguide.com/computing/mice/razer-viper-v4-pro-review">Razer Viper V4 Pro</a> ($159, 1.72oz).</p><p>What I love about the Sabre V2 Pro is that even though it’s as light as a feather, it’s still extremely sturdy, and it feels substantial under my palm. It’s made of high-quality plastic that looks and feels premium, and its skates effortlessly glide across most surfaces. Corsair happily includes spare skates in the box just in case the originals wear down. Also included in the box are pieces of anti-slip grip tape, which can be stuck to the mouse for additional grip.</p><p>Speaking of, the Sabre V2 Pro on its own feels very grippy. I didn’t feel like my fingers were slipping at any point during testing. The mouse measures 4.69 x 2.46 x 1.5 inches and I found it suitable for my medium-sized hands — although if you’ve got big hands, you may find the mouse a tad small (unless you’re a claw gripper). A chunkier mouse like the <a href="https://www.tomsguide.com/computing/peripherals/fnatic-x-lamzu-maya-8k-maya-x-8k-mouse-review">Fnatic x Lamzu Maya X 8K</a> ($119) or the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69) may be more suitable, if that’s the case.</p><h2 id="excellent-gaming-performance">Excellent gaming 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="guEdfrtduHNHXEz4cnVX2L" name="corsair_Sabre_V2_ 2.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/guEdfrtduHNHXEz4cnVX2L-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="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">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>The Corsair Sabre V2 Pro is a gaming mouse, and a good one at that. Packing a maximum of 33,000 DPI and 8,000Hz polling over wired and 2.4GHz wireless connections, the Sabre V2 Pro is an excellent choice for esports and FPS gamers. The polling rate can be dropped down to 1,000Hz using the mouse’s companion software (more on that shortly) if battery life is a concern (more on that later, too).</p><p>As I always do when testing gaming mice, I played a variety of games including Counter-Strike 2 and <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> on our testing lab’s gaming rig — and on the <a href="https://www.tomsguide.com/computing/gaming-laptops/msi-crosshair-16-hx-e14w-review">MSI Crosshair 16 HX E14W</a> laptop too, which I was testing alongside the mouse. I kicked things off with Counter-Strike 2. Thanks to 8,000Hz polling, I didn’t notice any latency issues or input lag, and all my actions were instantaneously registered.</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="iL4C24YwykcWXRv3JX6ujK" name="corsair_Sabre_V2_ 6.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/iL4C24YwykcWXRv3JX6ujK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 33,000 DPI is the real star here. Being able to swap between a low DPI of around 400 when aiming my sniper rifle at a target in the distance, and a high DPI of 8,000 or more when engaging in close-quarters combat gave me a competitive advantage. With the DPI set to 400, I was able to slow down my movements which helped me accurately aim at my target and execute headshots, especially when stealthily infiltrating enemy camps in Cyberpunk 2077.</p><p>Alongside rapid polling and DPI, you also get <a href="https://www.tomsguide.com/computing/what-is-angle-snapping">angle snapping</a> which can be enabled via Corsair Web Hub. In a nutshell, angle snapping smooths out mouse movements to ensure a straight line, thereby eliminating any jitters caused by hand shake. This helps you keep your cursor at head-level at all times which is useful in competitive games like Counter-Strike 2. I managed to take out a row of players as my cursor movements were stabilized and my crosshairs moved in a straighter line than they would if I didn’t have angle snapping toggled.</p><h2 id="web-based-companion-software">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/bw4Yvq2Jas7VpmWAuosiDe-1920-80.jpg" alt="Corsair Web Hub screenshot" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/jkDFi4gtpxcTKs5DWusiDe-1920-80.jpg" alt="Corsair Web Hub screenshot" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/duooKUk4H5Pgjajj7mTd9e-1920-80.jpg" alt="Corsair Web Hub screenshot" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure></figure><p>I love it when gadgets are accompanied by web-based software for customization as it negates the need for downloading proprietary apps or bloatware. The Corsair Sabre V2 Pro is compatible with <a href="https://www.corsair.com/web-hub/" target="_blank" rel="nofollow">Corsair Web Hub</a> which enables you to remap buttons, assign different DPI values to the five stages, adjust polling rate and lift height, and enable angle snapping. </p><p>Corsair Web Hub sports a clean interface and user-friendly design, with no unnecessary or intrusive ads or pop-ups. All you need to do is plug in your mouse using the included USB-C cable and launch Corsair Web Hub in Google Chrome, Edge, or other browser, and you’re good to go.</p><h2 id="competitively-priced-3">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="AakGuvFKFixX82qXSwMPtM" name="corsair_Sabre_V2_ 3.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/AakGuvFKFixX82qXSwMPtM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 don’t need to spend hundreds on a competent, competitive mouse, and the Corsair Sabre V2 Pro proves it. The mouse has a retail price of <a href="https://www.amazon.com/Corsair-Sabre-Ultralight-Wireless-Gaming/dp/B0FKJ2J2R8/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/Corsair-SABRE-Ultralight-Wireless-Gaming/dp/B0FPDBK2M6/" target="_blank" rel="nofollow">£89 at Amazon</a>, and I’ve seen it drop to $62 / £59 in the past, so it’s worth keeping an eye out during sales events. Even at its retail price, the Sabre V2 Pro is cheaper than other esports mice, like the Razer Viper V4 Pro ($159) and the <a href="https://www.tomsguide.com/computing/mice/logitech-g-pro-x2-superstrike-review">Logitech G Pro X2 Superstrike</a> ($179).</p><p>If you think the Sabre V2 Pro is out of your budget, both the Keychron M3 Mini V2 8K and the Keychron M6 8K cost $69 each. But Corsair’s offering is worth the extra spend if you want 33,000 DPI packed into an ultralight body.</p><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-the-downs"><span>Corsair Sabre V2 Pro review: The downs</span></h2><p>There isn’t a lot wrong with the Corsair Sabre V2 Pro but it’s worth noting that when polling at 8,000Hz, battery life doesn’t last as long, and there’s no dedicated DPI button.</p><h2 id="short-battery-life">Short battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Lj93fRzDyzW7UdyHNfJndN" name="corsair_Sabre_V2_ 5.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/Lj93fRzDyzW7UdyHNfJndN-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Sabre V2 Pro isn’t winning any awards when it comes to battery life, which can best be described as mediocre. Over 1,000Hz polling, you can get up to 70 hours between charges, which doesn’t sound too bad. But switch to 8,000Hz polling, and battery life drops to a measly 16 hours. After eight or so hours of playing, the battery had dropped to 45%, on course to die after the stated time.</p><p>This isn’t unexpected as 8KHz polling is a drain on both your system and the device, and you’ll find this with the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a>, too. If you want longer runtime, I’d recommend the Keychron M6 8K (35 hours) or the Keychron M3 Mini V2 8K (40 hours). The Razer Viper V4 Pro can also last up to 40 hours between charges but of course, it costs a lot more.</p><h2 id="no-onboard-dpi-switch">No onboard DPI switch</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="rqBGJAAT55Z8t4B7UAQzeL" name="corsair_Sabre_V2_ 4.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/rqBGJAAT55Z8t4B7UAQzeL-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 gripe with the Corsair Sabre V2 Pro is its lack of an onboard DPI switch. I’ve tested my fair share of mice — I always appreciate having a button on the mouse itself to quickly swap between different DPI values as in-game situations change. Unfortunately, doing so on the fly on the Sabre V2 Pro isn’t possible. To change the DPI, you’ll need to either launch the web-based companion software every time, or long-press the right-click and back button (located on the mouse’s left edge) for three seconds.</p><p>While the latter is simple enough to do, it’s time consuming and unnecessary, in my opinion, and it could have been made easier by simply pressing a dedicated DPI button. If you’re competing in an esports tournament, or playing competitively otherwise, you wouldn’t want to lose precious seconds, would you? This, on its own, stops me from outright recommending the Sabre V2 Pro.</p><p>If you want a gaming mouse that makes it easier to swap between DPI stages, I’d recommend the Keychron M6 8K, the Keychron M3 Mini V2 8K or the Fnatic x Lamzu Maya X 8K. The Razer Viper V4 Pro would be my personal top choice, but it costs nearly $60 more than the Sabre V2 Pro.</p><h2 class="article-body__section" id="section-corsair-sabre-v2-pro-review-verdict"><span>Corsair Sabre V2 Pro 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="PBBiVrkVEvg8Q4dSQi5c4M" name="corsair_Sabre_V2_ 7.JPG" alt="A black Corsair Sabre V2 Pro wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/PBBiVrkVEvg8Q4dSQi5c4M-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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’ve got a budget of $100 and want a powerful mouse, the Corsair Sabre V2 Pro is the answer. The mouse is ridiculously light and comfortable, and it glides effortlessly across surfaces. It’s built very well, and it’s accompanied by user-friendly web-based companion software. With 33,000 DPI and 8,000Hz polling at your disposal, the Sabre V2 Pro gives you a competitive edge in FPS and esports.</p><p>Few things in life are perfect, though, and the Sabre V2 Pro isn’t one of them. Its battery life isn’t the strongest, lasting just 16 hours when polling at 8,000Hz, and there’s no dedicated DPI button, so you have to fiddle with click combinations to change it on the fly.</p><p>If neither of those feel like dealbreakers, then the Sabre V2 Pro could be perfect for you. It’s sleek, comfortable and powerful, and it makes even the worst Counter-Strike 2 player (like myself) look half competent.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Upgrade your WFH setup: 6 ProtoArc ergonomic keyboard deals to save your wrists and your wallet ]]></title>
                                                                                                <dc:content><![CDATA[ <p>If you work at a computer all day long then you'll know just how much strain can be put on your wrists. </p><p>One good way to beat that is by using an ergonomic keyboard. These are designed to align with your body to reduce the risk of RSI and muscle strain. </p><p>One of the brands leading the way in keyboard design right now is ProtoArc, which has built a strong reputation by designing keyboards, mice, and other peripherals focused on both ergonomics and portability. </p><p>The six keyboards we've picked out here combine functionality with great design and good value. They're also all on sale at Amazon right now, so you can make some great savings.</p>        <div class="featured_product_block featured_block_hero" data-id="2fc08230-81ed-11f1-b8a8-57fb62ca226c">            <a href="https://www.amazon.com/dp/B0GTVZ23PM?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc KP50 Keyboard Wrist Rest" 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/ByUtcbRzi49SLVZLzDzDe6.jpg" alt="Protoarc Kp50 Keyboard Wrist Rest, Computer Ergonomic Wrist Rest for Keyboard With Slow Rebound Memory Foam and Non-Slip Silicone Base for Pain Relief, Desk, Home, Office, Work, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc KP50 Keyboard Wrist Rest</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08154-81ed-11f1-8b2a-51a942ab0540">            <a href="https://www.amazon.com/dp/B09TW35H1Z?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc XK01 Foldable Bluetooth Keyboard" 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/XBkW49FcG8FyjxyXdmJC86.jpg" alt="Protoarc Foldable Bluetooth Keyboard, Xk01 Full-Size Portable Wireless Keyboard for Travel, 105 Keys, 0.65" Keycaps, Multi-Device Sync for Ipad Tablet Smartphone Laptop Pc Windows Ios Android, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc XK01 Foldable Bluetooth Keyboard</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc081c2-81ed-11f1-85ca-8352ca50a74a">            <a href="https://www.amazon.com/dp/B0D3L4XFCQ?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc XK01 Foldable Keyboard with Touchpad" 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/eY8USg2fpfGxNba8HF9NS6.jpg" alt="Protoarc Foldable Keyboard With Touchpad, Xk01 Tp Folding Portable Keyboard, Full Size Keys, Large Trackpad Bluetooth Travel Keyboards for Laptop Pc Tablet - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc XK01 Foldable Keyboard with Touchpad</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08294-81ed-11f1-9276-3f3b84238207">            <a href="https://www.amazon.com/dp/B0F4N9H5DX?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc KM100-A Backlit Bluetooth Keyboard and Mouse for Mac" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:80.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/aW6XjryCezZ3wbbzmuWsq6.jpg" alt="Protoarc Km100-A Backlit Bluetooth Keyboard Mouse Combo for Mac, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc KM100-A Backlit Bluetooth Keyboard and Mouse for Mac</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08366-81ed-11f1-9bbb-fff2d1b177fc">            <a href="https://www.amazon.com/dp/B0DKNS3FK3?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc Backlit Foldable XK01 Keyboard" 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/TkCxSk8fU7tbEZmaqYVpK7.jpg" alt="Protoarc Backlit Foldable Keyboard, Xk01 Plus Folding Portable Bluetooth Keyboard for Travel, True Full Size External Wireless Keyboards With Backlight for Ipad Tablet Smartphone Laptop - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc Backlit Foldable XK01 Keyboard</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08302-81ed-11f1-b99d-057f6ecabfd4">            <a href="https://www.amazon.com/dp/B0GQ95XNQX?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc EKM02 Wave Keys Ergonomic Wireless Keyboard and Mouse" 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/DRbeZtnuwNibgkhnLPhH67.jpg" alt="Protoarc Ekm02 Wave Keys Ergonomic Wireless Keyboard and Mouse, Bluetooth Keyboard With Em11 Nl Ergonomic Vertical Mouse, Cushioned Palm Rest, Multi-Device, Rechargeable, for Windows, Mac - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc EKM02 Wave Keys Ergonomic Wireless Keyboard and Mouse</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/6-ergonomic-protoarc-keyboard-and-peripheral-deals-that-will-save-your-wrists-without-breaking-the-bank</link>
                                                                            <description>
                            <![CDATA[ ProtoArc has built a strong reputation for its ergonomically-designed keyboards and mice. We've picked out six of our favorites –all with money-off at Amazon now. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">7VJQZFSPQHbEbtY9uEsMWA</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/8VpyiKnWLs7dxXbc2HBo74-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 17 Jul 2026 16:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                                                                                    <dc:creator><![CDATA[ Will Salmon ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/HpRWP7Mh9ErgRGoTvZBbBJ-320-70.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/8VpyiKnWLs7dxXbc2HBo74-1920-80.jpg">
                                                            <media:credit><![CDATA[ProtoArc / Edited by Gemini]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A selection of ProtoArc ergonomic keyboards.]]></media:description>                                                            <media:text><![CDATA[A selection of ProtoArc ergonomic keyboards.]]></media:text>
                                <media:title type="plain"><![CDATA[A selection of ProtoArc ergonomic keyboards.]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/8VpyiKnWLs7dxXbc2HBo74-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>If you work at a computer all day long then you'll know just how much strain can be put on your wrists. </p><p>One good way to beat that is by using an ergonomic keyboard. These are designed to align with your body to reduce the risk of RSI and muscle strain. </p><p>One of the brands leading the way in keyboard design right now is ProtoArc, which has built a strong reputation by designing keyboards, mice, and other peripherals focused on both ergonomics and portability. </p><p>The six keyboards we've picked out here combine functionality with great design and good value. They're also all on sale at Amazon right now, so you can make some great savings.</p>        <div class="featured_product_block featured_block_hero" data-id="2fc08230-81ed-11f1-b8a8-57fb62ca226c">            <a href="https://www.amazon.com/dp/B0GTVZ23PM?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc KP50 Keyboard Wrist Rest" 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/ByUtcbRzi49SLVZLzDzDe6.jpg" alt="Protoarc Kp50 Keyboard Wrist Rest, Computer Ergonomic Wrist Rest for Keyboard With Slow Rebound Memory Foam and Non-Slip Silicone Base for Pain Relief, Desk, Home, Office, Work, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc KP50 Keyboard Wrist Rest</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08154-81ed-11f1-8b2a-51a942ab0540">            <a href="https://www.amazon.com/dp/B09TW35H1Z?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc XK01 Foldable Bluetooth Keyboard" 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/XBkW49FcG8FyjxyXdmJC86.jpg" alt="Protoarc Foldable Bluetooth Keyboard, Xk01 Full-Size Portable Wireless Keyboard for Travel, 105 Keys, 0.65" Keycaps, Multi-Device Sync for Ipad Tablet Smartphone Laptop Pc Windows Ios Android, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc XK01 Foldable Bluetooth Keyboard</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc081c2-81ed-11f1-85ca-8352ca50a74a">            <a href="https://www.amazon.com/dp/B0D3L4XFCQ?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc XK01 Foldable Keyboard with Touchpad" 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/eY8USg2fpfGxNba8HF9NS6.jpg" alt="Protoarc Foldable Keyboard With Touchpad, Xk01 Tp Folding Portable Keyboard, Full Size Keys, Large Trackpad Bluetooth Travel Keyboards for Laptop Pc Tablet - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc XK01 Foldable Keyboard with Touchpad</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08294-81ed-11f1-9276-3f3b84238207">            <a href="https://www.amazon.com/dp/B0F4N9H5DX?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc KM100-A Backlit Bluetooth Keyboard and Mouse for Mac" data-model-brand="" ><div class='product-image-widthsetter'><p class='vanilla-image-block' data-bordeaux-image-check style='padding-top:80.00%';><img style="width: 100%" class="featured_image" src="https://cdn.mos.cms.futurecdn.net/aW6XjryCezZ3wbbzmuWsq6.jpg" alt="Protoarc Km100-A Backlit Bluetooth Keyboard Mouse Combo for Mac, Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc KM100-A Backlit Bluetooth Keyboard and Mouse for Mac</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08366-81ed-11f1-9bbb-fff2d1b177fc">            <a href="https://www.amazon.com/dp/B0DKNS3FK3?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc Backlit Foldable XK01 Keyboard" 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/TkCxSk8fU7tbEZmaqYVpK7.jpg" alt="Protoarc Backlit Foldable Keyboard, Xk01 Plus Folding Portable Bluetooth Keyboard for Travel, True Full Size External Wireless Keyboards With Backlight for Ipad Tablet Smartphone Laptop - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc Backlit Foldable XK01 Keyboard</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>        <div class="featured_product_block featured_block_hero" data-id="2fc08302-81ed-11f1-b99d-057f6ecabfd4">            <a href="https://www.amazon.com/dp/B0GQ95XNQX?lv=shuf&channelId=500&plpRedirect=mhFallback" data-model-name="ProtoArc EKM02 Wave Keys Ergonomic Wireless Keyboard and Mouse" 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/DRbeZtnuwNibgkhnLPhH67.jpg" alt="Protoarc Ekm02 Wave Keys Ergonomic Wireless Keyboard and Mouse, Bluetooth Keyboard With Em11 Nl Ergonomic Vertical Mouse, Cushioned Palm Rest, Multi-Device, Rechargeable, for Windows, Mac - Black"></p></div></a>            <div class="featured_product_details_wrapper">                <div class="featured_product_title_wrapper">                                        <div class='featured__brand'>ProtoArc</div>                                        <div class="featured__title">ProtoArc EKM02 Wave Keys Ergonomic Wireless Keyboard and Mouse</div>                                    </div>                <div class="subtitle__description">                                                            <p></p>                </div>                            </div>        </div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The Razer Huntsman V3 just became my go-to PC gaming keyboard — here’s why ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I haven’t used a Razer gaming keyboard for a while, so testing the new Razer Huntsman V3 TKL 8KHz feels like reuniting with an old friend. Since the company uses proprietary switches, its keyboards feel different from competitors that use third-party parts. This remains true for the Huntsman V3.</p><p>This more affordable variant of last year’s Huntsman V3 Pro TKL 8KHz packs several competitive gaming-focused features, such as super-fast 8KHz polling and adjustable actuation points. While the keyboard is relatively thin, its aluminum build makes it sturdy and safe to travel with. And while it’s a gaming keyboard at its core, it’s also great for typing.</p><p>If you’re a competitive gamer who needs a keyboard that can keep up with you and won’t break the bank, the Razer Huntsman V3 Tenkeyless 8KHz is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> that's worth checking out. Here’s why.</p><div class="product"><a data-dimension112="56b2384a-814b-11f1-b86a-11d34ca164b1" data-action="Deal Block" data-label="Huntsman V3 Tenkeyless 8KHz" data-dimension48="Huntsman V3 Tenkeyless 8KHz" data-dimension25="$169" href="https://www.amazon.com/Razer-Huntsman-Esports-Gaming-Keyboard/dp/B0GWNHWWN8" 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:100.00%;"><img id="GQZWYf9pyTNyTxNzdKcnQh" name="Razer Huntsman V3 Tenkeyless 8KHz" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GQZWYf9pyTNyTxNzdKcnQh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Razer Huntsman V3 Tenkeyless 8KHz is a pro-grade gaming keyboard, featuring precise optical switches, a super-high 8K polling rate, adjustable actuation points, and full RGB lighting. At $169, it's the most affordable Huntsman V3 model currently available, and it's worth the money if you're a hardcore competitive PC gamer.<a class="view-deal button" href="https://www.amazon.com/Razer-Huntsman-Esports-Gaming-Keyboard/dp/B0GWNHWWN8" target="_blank" rel="nofollow" data-dimension112="56b2384a-814b-11f1-b86a-11d34ca164b1" data-action="Deal Block" data-label="Huntsman V3 Tenkeyless 8KHz" data-dimension48="Huntsman V3 Tenkeyless 8KHz" data-dimension25="$169">View Deal</a></p></div><h2 id="what-s-this-razer-huntsman-v3-all-about">What's this Razer Huntsman V3 all about?</h2><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="qfm62vGH79v4W9e5Ythv4U" name="Razer Huntsman V3 TKL 8K--1" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/qfm62vGH79v4W9e5Ythv4U-1920-80.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 Razer Huntsman V3 TKL 8KHz is a competitive gaming keyboard built for fast-paced first-person shooters and other genres that demand quick reflexes. It utilizes the company’s proprietary analog optical switches, 8K “HyperPolling,” adjustable actuation, and a slew of RGB customization options.</p><p><a href="https://www.tomsguide.com/computing/peripherals/i-finally-tried-magnetic-key-switches-but-i-never-thought-id-find-them-in-a-split-keyboard-thats-also-wireless">Magnetic switches</a> have been all the rage in recent years, but Razer has remained true to its optical switches. Both switch types provide faster response times than traditional mechanical switches, which is why competitive gamers tend to gravitate toward them. I won’t debate whether optical or magnetic switches are better for gaming, but I will say Razer’s optical switches are insanely responsive.</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="pCtyQhZn2pFFSxbjm22wGd" name="Razer Huntsman V3 TKL 8K--3" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/pCtyQhZn2pFFSxbjm22wGd-1920-80.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>Like with magnetic switches, you can set multiple actuation points on a single key—two actuation points in this case. For example, you can have an actuation point of 2.0 mm on the W key to walk and 4.0 to run. Here, you can set actuation points between 0.1 mm and 4.0, which is standard for both magnetic and optical keyboards.</p><p>Then there’s the 8K polling (the speed at which the keyboard sends signals to your computer), which Razer says is faster than other keyboards claiming such a high polling rate. According to the company, most keyboards with 8K polling have a 1.6ms response time, but the Huntsman V3 has a faster 0.48ms response time.</p><p>So, how does all of that translate to real-world use? Though I’m nowhere near a competitive gamer, even I could feel the overall smoothness and fast response when playing a first-person shooter like <a href="https://www.tomsguide.com/gaming/doom-the-dark-ages-is-a-heavy-metal-album-cover-come-to-life-and-i-cant-wait-to-play-ithttps://www.tomsguide.com/gaming/doom-the-dark-ages-review">Doom: The Dark Ages</a>. I especially like setting a faster actuation point for shooting since I could lightly brush the key instead of pressing it down all the way. And in case you’re wondering, you can set the actuation points right on the keyboard, though I still preferred doing so via the Razer Synapse app on PC.</p><h2 id="the-experience">The experience</h2><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="HijJ7EnLSEG77tMhHZry8n" name="Razer Huntsman V3 TKL 8K--5" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/HijJ7EnLSEG77tMhHZry8n-1920-80.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>While I’m not the target customer for a competitive gaming keyboard, I enjoy using them because of their fast response time and smooth keystrokes—which the Razer Huntsman V3 has in spades. While the blazing performance and high customization are fantastic, there’s more to this keyboard than those selling points.</p><p>The elevated keys, with their PBT-coated keycaps, are a joy to type on. One thing I especially appreciate as someone with a heavy touch is how the keys have more resistance the further you press them. The tenkeyless layout is also ideal for me since I don’t need a numberpad, making the board unnecessarily larger.</p><p>The thocky sound the keys produce also makes using the keyboard more enjoyable. Sure, the sound isn’t as deep as you’d find on mechanical keyboards, but it’s still good. While not a clicky board, it’s definitely noisy, so you might not want to use it in an office or anywhere that could annoy others (though I used it in the office 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:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uuNMe4PGQ9m4HzhL9F2pBB" name="Razer Huntsman V3 TKL 8K--6" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/uuNMe4PGQ9m4HzhL9F2pBB-1920-80.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 aluminum frame not only helps give the keyboard a richer sound, but also makes it sturdy and durable. That’s important if you need to travel with the Huntsman V3. The brushed aluminum is a nice touch that gives the keyboard a more premium look without being overbearing. And while this is a gaming keyboard, it doesn’t look like one thanks to its overall subdued design.</p><p>Of course, if you want more of a gamer aesthetic, there is no shortage of RGB options to toy around with. I’m somewhat neutral on RGB lighting, but I like that folks can customize the lighting to their exact preference. This is Razer, so the company isn’t skimping on that front.</p><p>Lastly, this isn't a wireless keyboard, so you'll need to use a USB-A port on your PC. That said, the braided cable is detachable, which is good if you need to take the keyboard on the road.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eEqDge"></div>                            </div>                            <script src="https://kwizly.com/embed/eEqDge.js" async></script><h2 id="outlook">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="wCUPYccXjWrNm5u7hH2xcK" name="Razer Huntsman V3 TKL 8K--7" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/wCUPYccXjWrNm5u7hH2xcK-1920-80.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 Razer Huntsman V3 Tenkeyless 8K is an excellent gaming keyboard thanks to its robust, competitive gamer-focused features. Outside of gaming, it also delivers a nice typing experience thanks to its durable build and responsive keys. It’s a bit pricey at $169, but for what it offers, you’re definitely getting your money’s worth.</p><p>Again, I'm a fan of Razer keyboards, and this one has everything I've come to expect from the company's peripherals. While the Pro versions of this keyboard might have more features, I like the simplicity of this board, as it still delivers excellent performance. At $169, it's worth considering.</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/computing/keyboards/logitech-g-g512-x-review">I became a keyboard tinkerer with the Logitech G G512 X</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/i-just-got-my-first-gaming-keyboard-heres-why-ill-never-use-a-regular-board-again">I just got my first gaming keyboard — I’ll never use a regular board again</a></li><li><a href="https://www.tomsguide.com/computing/buying-a-gaming-keyboard-rapid-trigger-and-snap-tap-explained">Buying a gaming keyboard? Rapid Trigger and Snap Tap explained</a></li></ul><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=gaming+laptops&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, .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/computing/keyboards/the-razer-huntsman-v3-just-became-my-go-to-pc-gaming-keyboard-heres-why</link>
                                                                            <description>
                            <![CDATA[ The Razer Huntsman V3 Tenkeyless 8KHz packs a ton of pro-grade features at a relatively affordable price. Here's what makes it worth considering. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">KGkcnrUvYLHwpfHSiUEeV4</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/iF3DdXTMBxzpFoVncGnzSK-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 17 Jul 2026 08:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Gaming Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Gaming]]></category>
                                                                                                <author><![CDATA[ tony.polanco@futurenet.com (Tony Polanco) ]]></author>                    <dc:creator><![CDATA[ Tony Polanco ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/atzRNqFt5wYgEUPBDahWsD-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ null ]]></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/iF3DdXTMBxzpFoVncGnzSK-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Razer Huntsman V3 Tenkeyless 8KHz]]></media:description>                                                            <media:text><![CDATA[Razer Huntsman V3 Tenkeyless 8KHz]]></media:text>
                                <media:title type="plain"><![CDATA[Razer Huntsman V3 Tenkeyless 8KHz]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/iF3DdXTMBxzpFoVncGnzSK-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I haven’t used a Razer gaming keyboard for a while, so testing the new Razer Huntsman V3 TKL 8KHz feels like reuniting with an old friend. Since the company uses proprietary switches, its keyboards feel different from competitors that use third-party parts. This remains true for the Huntsman V3.</p><p>This more affordable variant of last year’s Huntsman V3 Pro TKL 8KHz packs several competitive gaming-focused features, such as super-fast 8KHz polling and adjustable actuation points. While the keyboard is relatively thin, its aluminum build makes it sturdy and safe to travel with. And while it’s a gaming keyboard at its core, it’s also great for typing.</p><p>If you’re a competitive gamer who needs a keyboard that can keep up with you and won’t break the bank, the Razer Huntsman V3 Tenkeyless 8KHz is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> that's worth checking out. Here’s why.</p><div class="product"><a data-dimension112="56b2384a-814b-11f1-b86a-11d34ca164b1" data-action="Deal Block" data-label="Huntsman V3 Tenkeyless 8KHz" data-dimension48="Huntsman V3 Tenkeyless 8KHz" data-dimension25="$169" href="https://www.amazon.com/Razer-Huntsman-Esports-Gaming-Keyboard/dp/B0GWNHWWN8" 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:100.00%;"><img id="GQZWYf9pyTNyTxNzdKcnQh" name="Razer Huntsman V3 Tenkeyless 8KHz" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GQZWYf9pyTNyTxNzdKcnQh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Razer Huntsman V3 Tenkeyless 8KHz is a pro-grade gaming keyboard, featuring precise optical switches, a super-high 8K polling rate, adjustable actuation points, and full RGB lighting. At $169, it's the most affordable Huntsman V3 model currently available, and it's worth the money if you're a hardcore competitive PC gamer.<a class="view-deal button" href="https://www.amazon.com/Razer-Huntsman-Esports-Gaming-Keyboard/dp/B0GWNHWWN8" target="_blank" rel="nofollow" data-dimension112="56b2384a-814b-11f1-b86a-11d34ca164b1" data-action="Deal Block" data-label="Huntsman V3 Tenkeyless 8KHz" data-dimension48="Huntsman V3 Tenkeyless 8KHz" data-dimension25="$169">View Deal</a></p></div><h2 id="what-s-this-razer-huntsman-v3-all-about">What's this Razer Huntsman V3 all about?</h2><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="qfm62vGH79v4W9e5Ythv4U" name="Razer Huntsman V3 TKL 8K--1" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/qfm62vGH79v4W9e5Ythv4U-1920-80.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 Razer Huntsman V3 TKL 8KHz is a competitive gaming keyboard built for fast-paced first-person shooters and other genres that demand quick reflexes. It utilizes the company’s proprietary analog optical switches, 8K “HyperPolling,” adjustable actuation, and a slew of RGB customization options.</p><p><a href="https://www.tomsguide.com/computing/peripherals/i-finally-tried-magnetic-key-switches-but-i-never-thought-id-find-them-in-a-split-keyboard-thats-also-wireless">Magnetic switches</a> have been all the rage in recent years, but Razer has remained true to its optical switches. Both switch types provide faster response times than traditional mechanical switches, which is why competitive gamers tend to gravitate toward them. I won’t debate whether optical or magnetic switches are better for gaming, but I will say Razer’s optical switches are insanely responsive.</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="pCtyQhZn2pFFSxbjm22wGd" name="Razer Huntsman V3 TKL 8K--3" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/pCtyQhZn2pFFSxbjm22wGd-1920-80.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>Like with magnetic switches, you can set multiple actuation points on a single key—two actuation points in this case. For example, you can have an actuation point of 2.0 mm on the W key to walk and 4.0 to run. Here, you can set actuation points between 0.1 mm and 4.0, which is standard for both magnetic and optical keyboards.</p><p>Then there’s the 8K polling (the speed at which the keyboard sends signals to your computer), which Razer says is faster than other keyboards claiming such a high polling rate. According to the company, most keyboards with 8K polling have a 1.6ms response time, but the Huntsman V3 has a faster 0.48ms response time.</p><p>So, how does all of that translate to real-world use? Though I’m nowhere near a competitive gamer, even I could feel the overall smoothness and fast response when playing a first-person shooter like <a href="https://www.tomsguide.com/gaming/doom-the-dark-ages-is-a-heavy-metal-album-cover-come-to-life-and-i-cant-wait-to-play-ithttps://www.tomsguide.com/gaming/doom-the-dark-ages-review">Doom: The Dark Ages</a>. I especially like setting a faster actuation point for shooting since I could lightly brush the key instead of pressing it down all the way. And in case you’re wondering, you can set the actuation points right on the keyboard, though I still preferred doing so via the Razer Synapse app on PC.</p><h2 id="the-experience">The experience</h2><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="HijJ7EnLSEG77tMhHZry8n" name="Razer Huntsman V3 TKL 8K--5" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/HijJ7EnLSEG77tMhHZry8n-1920-80.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>While I’m not the target customer for a competitive gaming keyboard, I enjoy using them because of their fast response time and smooth keystrokes—which the Razer Huntsman V3 has in spades. While the blazing performance and high customization are fantastic, there’s more to this keyboard than those selling points.</p><p>The elevated keys, with their PBT-coated keycaps, are a joy to type on. One thing I especially appreciate as someone with a heavy touch is how the keys have more resistance the further you press them. The tenkeyless layout is also ideal for me since I don’t need a numberpad, making the board unnecessarily larger.</p><p>The thocky sound the keys produce also makes using the keyboard more enjoyable. Sure, the sound isn’t as deep as you’d find on mechanical keyboards, but it’s still good. While not a clicky board, it’s definitely noisy, so you might not want to use it in an office or anywhere that could annoy others (though I used it in the office 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:2000px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uuNMe4PGQ9m4HzhL9F2pBB" name="Razer Huntsman V3 TKL 8K--6" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/uuNMe4PGQ9m4HzhL9F2pBB-1920-80.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 aluminum frame not only helps give the keyboard a richer sound, but also makes it sturdy and durable. That’s important if you need to travel with the Huntsman V3. The brushed aluminum is a nice touch that gives the keyboard a more premium look without being overbearing. And while this is a gaming keyboard, it doesn’t look like one thanks to its overall subdued design.</p><p>Of course, if you want more of a gamer aesthetic, there is no shortage of RGB options to toy around with. I’m somewhat neutral on RGB lighting, but I like that folks can customize the lighting to their exact preference. This is Razer, so the company isn’t skimping on that front.</p><p>Lastly, this isn't a wireless keyboard, so you'll need to use a USB-A port on your PC. That said, the braided cable is detachable, which is good if you need to take the keyboard on the road.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eEqDge"></div>                            </div>                            <script src="https://kwizly.com/embed/eEqDge.js" async></script><h2 id="outlook">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="wCUPYccXjWrNm5u7hH2xcK" name="Razer Huntsman V3 TKL 8K--7" alt="Razer Huntsman V3 Tenkeyless 8KHz" src="https://cdn.mos.cms.futurecdn.net/wCUPYccXjWrNm5u7hH2xcK-1920-80.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 Razer Huntsman V3 Tenkeyless 8K is an excellent gaming keyboard thanks to its robust, competitive gamer-focused features. Outside of gaming, it also delivers a nice typing experience thanks to its durable build and responsive keys. It’s a bit pricey at $169, but for what it offers, you’re definitely getting your money’s worth.</p><p>Again, I'm a fan of Razer keyboards, and this one has everything I've come to expect from the company's peripherals. While the Pro versions of this keyboard might have more features, I like the simplicity of this board, as it still delivers excellent performance. At $169, it's worth considering.</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/computing/keyboards/logitech-g-g512-x-review">I became a keyboard tinkerer with the Logitech G G512 X</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/i-just-got-my-first-gaming-keyboard-heres-why-ill-never-use-a-regular-board-again">I just got my first gaming keyboard — I’ll never use a regular board again</a></li><li><a href="https://www.tomsguide.com/computing/buying-a-gaming-keyboard-rapid-trigger-and-snap-tap-explained">Buying a gaming keyboard? Rapid Trigger and Snap Tap explained</a></li></ul><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=gaming+laptops&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, .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[ The Razer Viper V4 Pro is ‘nothing short of phenomenal’ — its industry-leading 50,000 DPI and 8,000Hz polling are overkill in all the right ways ]]></title>
                                                                                                <dc:content><![CDATA[ <p>No loud design that draws unnecessary eyeballs to your desk. No flashy, garish RGB. Just pure precision and performance: that’s what the Razer Viper V4 Pro stands for. This is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a> for esports players seeking speed above all else. The Viper V4 Pro packs industry-leading 50,000 DPI as well as 8,000Hz polling to deliver phenomenal performance in FPS titles.</p><p>Customization is quick and easy via Razer Synapse, and the mouse itself is lightweight, sturdy and comfortable. I’ve had a thoroughly enjoyable time testing the Viper V4 Pro, and if you can afford it, this is <em>the </em>mouse to get for competitive gaming.</p><p>For the complete breakdown, read my full Razer Viper V4 Pro review.</p><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-specs"><span>Razer Viper V4 Pro 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/Razer-Viper-Wireless-Esports-Gaming/dp/B0GMLBSSTD/" target="_blank" rel="nofollow">$159</a> / <a href="https://www.amazon.co.uk/Razer-Viper-Pro-sports-Ultra-lightweight-Black/dp/B0G5QBTC2Q/" target="_blank" rel="nofollow">£159</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>50,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>6</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>5 x 2.51 x 1.57 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.72oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 2.4GHz dongle</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>180 hours (1,000Hz) | 45 hours (8,000Hz)</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-cheat-sheet"><span>Razer Viper V4 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A right-handed gaming mouse with 50,000 DPI and 8,000Hz polling</li><li><strong>Who is it for? </strong>Esports and competitive gamers who value precision above all else</li><li><strong>How much does it cost?</strong> <a href="https://www.amazon.com/Razer-Viper-Wireless-Esports-Gaming/dp/B0GMLBSSTD/" target="_blank" rel="nofollow">$159</a> / <a href="https://www.amazon.co.uk/Razer-Viper-Pro-sports-Ultra-lightweight-Black/dp/B0G5QBTC2Q/" target="_blank" rel="nofollow">£159</a></li><li><strong>What do we like? </strong>The lightweight design, responsive and phenomenal gaming performance, long battery life, and user-friendly software</li><li><strong>What don’t we like? </strong>The loud clicks… but that’s about it</li></ul><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-the-ups"><span>Razer Viper V4 Pro review: The ups</span></h2><p>From its lightweight design and industry-leading specs, to its powerful gaming performance and user-friendly companion software, the Razer Viper V4 Pro doesn’t give you much to complain about.</p><h2 id="light-as-a-feather">Light as a feather</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BAu3FB8SnDqwCToRcrcbSM" name="Razer_Viper_V4_Pro_Mouse_004.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/BAu3FB8SnDqwCToRcrcbSM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 time of writing, the Viper V4 Pro is Razer’s most recent mouse to join the lightweight, sub-50g club. The mouse weighs just 49g or 1.72oz so it doesn’t feel like it’s weighing down your bag when you’re traveling with it. Because the mouse is so light, it requires less physical effort to move, stop and change direction. That also means your hand and wrist don’t feel fatigued during long gaming sessions, making the Viper V4 Pro a great choice for competitive players.</p><p>I’ve tested plenty of mice in my time, and the Viper V4 Pro is one of the lightest rodents I’ve used. It’s even lighter than the <a href="https://www.tomsguide.com/computing/mice/logitech-g-pro-x2-superstrike-review">Logitech G Pro X2 Superstrike</a> ($179, 2.15oz), our top pick for the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a>. The Viper V4 Pro trumps other ultra-lightweight mice too, such as the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini V2 8K</a> ($69, 1.94oz) and the <a href="https://www.tomsguide.com/computing/peripherals/nzxt-lift-elite-wireless-gaming-mouse-review">NZXT Lift Elite Wireless</a> ($79, 2.01oz).</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="5T9WQojfvaNJzVoS2t6XUM" name="Razer_Viper_V4_Pro_Mouse_011.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/5T9WQojfvaNJzVoS2t6XUM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 just because the Viper V4 Pro is as light as a feather doesn’t mean it isn’t sturdy. Made of high-quality plastic, the Viper V4 Pro feels grippy to hold straight out of the box. Razer happily includes anti-slip grip tape in the box that you can stick to the mouse for more grip. The rodent measures 5 x 2.51 x 1.57 inches and feels substantial to hold for those with medium-sized hands — like I do. Both claw and palm grips work with the Viper V4 Pro, too, making it a versatile pick.</p><h2 id="industry-leading-specs">Industry-leading specs</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="627gTmqh2XLbYRfaoL2TTM" name="Razer_Viper_V4_Pro_Mouse_003.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/627gTmqh2XLbYRfaoL2TTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>How much is too much DPI? Certainly not 50,000, according to Razer. That’s right: the Razer Viper V4 Pro boasts a maximum DPI of 50,000. That’s a <em>lot</em>, and at the time of writing, it’s industry-leading. We haven’t tested any other mice that beat the Viper V4 Pro. The Logitech G Pro X2 Superstrike comes close with its 44,000 DPI, though. I’d say both are overkill because who <em>really </em>needs <em>that </em>much speed? It’s there if you do, of course.</p><p>Changing the DPI is extremely easy. The Viper V4 Pro has a DPI button located on its underside, and by pressing it, you can cycle through 400, 800, 1600, 3200 and 6400 DPI by default. These can be customized via Razer Synapse (more on that shortly). I appreciate being able to adjust the DPI on the fly, as not being able to do so was one of my biggest gripes with the <a href="https://www.tomsguide.com/gaming/peripherals/logitech-g-pro-x-superlight-2-review">Logitech G Pro X Superlight 2</a> ($159).</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="txJb4CxAEFFrcTQ7yQ2JTM" name="Razer_Viper_V4_Pro_Mouse_009.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/txJb4CxAEFFrcTQ7yQ2JTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Aiding the Viper V4 Pro’s gaming prowess is its 8,000Hz polling rate which you can get over both wired and 2.4GHz wireless connections. The mouse is accompanied by a dome-shaped dongle which needs to be plugged into your machine with a USB-C cable. It’s fitted with three LED indicators which are color-coded to indicate the polling rate, connection stability, and battery level. There’s no Bluetooth but the lack of it on an esports mouse isn’t a major drawback.</p><h2 id="phenomenal-gaming-performance">Phenomenal gaming 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="sa62sd6TVMRtES8h4z6ZUM" name="Razer_Viper_V4_Pro_Mouse_010.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/sa62sd6TVMRtES8h4z6ZUM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="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">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>In theory, the Razer Viper V4 Pro sounds incredible, but does it perform as well in real-world use? Hell yeah, it does. I’ll preface this by saying that I’m not a professional esports player, but even I felt like the Viper V4 Pro gave me a competitive advantage in every game I played. Counter-Strike 2? Walk in the park. Cyberpunk 2077? Easy as pie. The Viper V4 Pro never once let me down throughout my testing.</p><p>I kicked things off by playing a few rounds of Counter-Strike 2. Admittedly, I’ve never been great at the game, but the Viper V4 Pro ensured I performed to the best of my ability. 8,000Hz polling meant that I didn’t experience any latency issues and all of my commands were instantaneously registered.</p><p>What I love about the Viper V4 Pro is its adaptive polling rate which, once enabled through Razer Synapse, boosts the polling up to 8KHz when gaming and drops it back to 1KHz when you’re doing non-gaming things on your computer. This helps preserve the mouse’s 180-hour battery, and it worked as intended in my 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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xYMH9uwVg3icpQUkK4AfSM" name="Razer_Viper_V4_Pro_Mouse_005.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/xYMH9uwVg3icpQUkK4AfSM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>So, how about that 50,000 DPI, eh? It is the headline feature, after all. I can’t see myself using such high DPI but it might be beneficial for some. Personally, I find DPI around the 10,000 mark more than sufficient for snappy movements. In Counter-Strike 2 and Cyberpunk 2077, anytime I was engaging in close-quarters combat, I upped the DPI to 10,000 (after assigning it via Razer Synapse). This made my movements snappy and rapid, and I was able to quickly cut down big groups of enemies with my knife.</p><p>Being able to change the DPI on the fly came in clutch when I went from heated combat to stealthy gameplay. Infiltrating heavily-guarded bases in Cyberpunk 2077 and Hitman felt like a walk in the park, thanks to a low DPI of 500 enabling me to execute precise headshots with my silenced pistol. It was especially useful when aiming at just one target surrounded by several other NPCs. Similarly, using a long-range sniper rifle to do the same felt effortless.</p><p>Last but not least, the Viper V4 Pro’s optical switches feel amazing to press. Razer says they require 12% lighter actuation compared to the ones you’ll find in the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/razers-new-viper-v3-pro-gaming-mouse-boasts-an-obscene-8000hz-polling-rate">Viper V3 Pro</a> ($149) so they’re easier (and less fatiguing) to activate or spam-click. I love how much tactile feedback they provide as well.</p><h2 id="long-battery-life">Long battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="S3iD8qufmtaS2S6gz4kKTM" name="Razer_Viper_V4_Pro_Mouse_006.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/S3iD8qufmtaS2S6gz4kKTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 cherry on top of the cake is the Razer Viper V4 Pro’s long battery life. Razer says that the mouse can last 180 hours (1,000Hz polling) or 45 hours (8,000Hz polling). Because 8,000Hz polling is a drain on not just system resources but also the mouse, adaptive polling rate is highly beneficial as it helps make the Viper V4 Pro last as long as possible.</p><p>Thanks to this, the Viper V4 Pro upstages its competitors. The Logitech G Pro X2 Superstrike is rated for 90 hours; the Logitech G Pro X Superlight 2 is rated for 95 hours; and the Keychron M3 Mini V2 8K can last up to 140 hours. I couldn’t kill the Viper V4 Pro entirely in my testing, but after three hours of gaming with 8KHz polling enabled and another three working over 1,000Hz, the battery had dipped by only 8%. If it’s long battery life you’re after, the Viper V4 Pro is the way to go.</p><h2 id="user-friendly-companion-software-2">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/NPK6DL7v3TumPmUkiUaZsD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/6pEkWogjESw6xQyD5nvSpD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Ht4YkFXpitvabNJuFEuTkD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/qYnzKtjJ5Lra3KaADCPodD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure></figure><p>To customize the Razer Viper V4 Pro, you’ll need to download <a href="https://www.razer.com/gb-en/synapse-4" target="_blank" rel="nofollow">Razer Synapse 4</a> on either Windows or macOS. It’s your one-stop shop for personalizing most Razer peripherals. The software sports a clean interface and is easy to navigate.</p><p>It allows you to assign different DPI values to the Viper V4 Pro’s DPI button, adjust the polling rate and toggle adaptive polling, remap the buttons, and customize lift-off distance. It’s worth taking a few minutes and familiarizing yourself with the different settings so that you can take advantage of everything the Viper V4 Pro has to offer.</p><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-the-downs"><span>Razer Viper V4 Pro review: The downs</span></h2><p>There isn’t a lot wrong with the Razer Viper V4 Pro, but its loud clicks and steep price may not be for everyone.</p><h2 id="loud-clicks-won-t-be-for-everyone">Loud clicks won’t be for everyone</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Tno2e3NhjuKSTXrJim3ySM" name="Razer_Viper_V4_Pro_Mouse_008.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/Tno2e3NhjuKSTXrJim3ySM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 second thing I noticed about the Razer Viper V4 Pro after its lightweight design was the sound its left- and right-buttons make when pressed. And <em>boy</em>, are they loud. My colleagues instantly noticed it too, and said that the loud sound wasn’t their cup of tea.</p><p>This is all to say that if you prefer a quiet mouse, like the SteelSeries Aerox 3 Wireless Gen 2, the Viper V4 Pro might not be for you — although given how powerful it is, it’s a compromise I’d be willing to make.</p><h2 id="expensive-but-worth-it-for-esports-players">Expensive (but worth it for esports players)</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Gw3GmEp7AXDUUwT9qQZrTM" name="Razer_Viper_V4_Pro_Mouse_012.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/Gw3GmEp7AXDUUwT9qQZrTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 more of a pre-purchase consideration rather than an outright flaw: the Razer Viper V4 Pro isn’t for the budget conscious. The mouse retails for <a href="https://www.amazon.com/Razer-Viper-Wireless-Esports-Gaming/dp/B0GMLBSSTD/" target="_blank" rel="nofollow">$159</a> / <a href="https://www.amazon.co.uk/Razer-Viper-Pro-sports-Ultra-lightweight-Black/dp/B0G5QBTC2Q/" target="_blank" rel="nofollow">£159 at Amazon</a> so it’s on the pricey side of things. Esports mice, however, are usually expensive and the Viper V4 Pro isn’t beyond the pale here.</p><p>The Viper V4 Pro costs the same as the Logitech G Pro X Superlight 2 ($159) and is slightly cheaper than the Logitech G Pro X2 Superstrike ($179). But if you aren’t fussed about 50,000 DPI, there are plenty of sub-$100 alternatives available. The Keychron M3 Mini V2 8K ($69) packs 8,000Hz polling and 30,000 DPI; the NZXT Lift Elite Wireless ($79) maxes out at 26,000 DPI; and the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69) combines ergonomic benefits with powerful gaming features.</p><p>Unless you’re a competitive and professional esports player, there’s little reason to splash out on the Viper V4 Pro. But if you want a mouse that’s at the top of its game, few are better than Razer’s flagship model.</p><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-verdict"><span>Razer Viper V4 Pro 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="qReRy9buAtnk7CSXmCZHSM" name="Razer_Viper_V4_Pro_Mouse_002.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/qReRy9buAtnk7CSXmCZHSM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 can’t recall the last time I had so much fun testing a mouse. The Razer Viper V4 Pro sets a new gold standard for gaming mice with its 50,000 DPI and 8,000Hz polling rate. It’s excellent for FPS titles as well as RPGs, and class-leading battery life goes a long way to justify its premium price tag.</p><p>The Viper V4 Pro is extremely light and comfortable too, and it sports a low-key, minimalist design that doesn’t draw eyeballs to your desk… though the loud clicks might. Its optical switches provide great tactile feedback when pressed, and user-friendly companion software makes customization quick and easy.</p><p>Though casual gamers are unlikely to benefit from features like the 50,000 DPI, competitive gamers looking for a fast, light and capable mouse should look no further than the Viper V4 Pro. It’s simply phenomenal.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/mice/razer-viper-v4-pro-review</link>
                                                                            <description>
                            <![CDATA[ The Razer Viper V4 Pro is a game-changer for esports, thanks to its 8,000Hz polling and 50,000 DPI. Gaming performance is superb while customization is easy. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">zjhq8uBWnzKBCGsfdAuENW</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/Vivt5mMLcVcYGm4WWGVpzL-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 06 Jul 2026 09:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Mice]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/Vivt5mMLcVcYGm4WWGVpzL-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Razer Viper V4 Pro mouse]]></media:description>                                                            <media:text><![CDATA[A black Razer Viper V4 Pro mouse]]></media:text>
                                <media:title type="plain"><![CDATA[A black Razer Viper V4 Pro mouse]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/Vivt5mMLcVcYGm4WWGVpzL-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>No loud design that draws unnecessary eyeballs to your desk. No flashy, garish RGB. Just pure precision and performance: that’s what the Razer Viper V4 Pro stands for. This is one of the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a> for esports players seeking speed above all else. The Viper V4 Pro packs industry-leading 50,000 DPI as well as 8,000Hz polling to deliver phenomenal performance in FPS titles.</p><p>Customization is quick and easy via Razer Synapse, and the mouse itself is lightweight, sturdy and comfortable. I’ve had a thoroughly enjoyable time testing the Viper V4 Pro, and if you can afford it, this is <em>the </em>mouse to get for competitive gaming.</p><p>For the complete breakdown, read my full Razer Viper V4 Pro review.</p><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-specs"><span>Razer Viper V4 Pro 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/Razer-Viper-Wireless-Esports-Gaming/dp/B0GMLBSSTD/" target="_blank" rel="nofollow">$159</a> / <a href="https://www.amazon.co.uk/Razer-Viper-Pro-sports-Ultra-lightweight-Black/dp/B0G5QBTC2Q/" target="_blank" rel="nofollow">£159</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>50,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>6</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>5 x 2.51 x 1.57 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.72oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired, 2.4GHz dongle</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>180 hours (1,000Hz) | 45 hours (8,000Hz)</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-cheat-sheet"><span>Razer Viper V4 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A right-handed gaming mouse with 50,000 DPI and 8,000Hz polling</li><li><strong>Who is it for? </strong>Esports and competitive gamers who value precision above all else</li><li><strong>How much does it cost?</strong> <a href="https://www.amazon.com/Razer-Viper-Wireless-Esports-Gaming/dp/B0GMLBSSTD/" target="_blank" rel="nofollow">$159</a> / <a href="https://www.amazon.co.uk/Razer-Viper-Pro-sports-Ultra-lightweight-Black/dp/B0G5QBTC2Q/" target="_blank" rel="nofollow">£159</a></li><li><strong>What do we like? </strong>The lightweight design, responsive and phenomenal gaming performance, long battery life, and user-friendly software</li><li><strong>What don’t we like? </strong>The loud clicks… but that’s about it</li></ul><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-the-ups"><span>Razer Viper V4 Pro review: The ups</span></h2><p>From its lightweight design and industry-leading specs, to its powerful gaming performance and user-friendly companion software, the Razer Viper V4 Pro doesn’t give you much to complain about.</p><h2 id="light-as-a-feather">Light as a feather</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BAu3FB8SnDqwCToRcrcbSM" name="Razer_Viper_V4_Pro_Mouse_004.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/BAu3FB8SnDqwCToRcrcbSM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 time of writing, the Viper V4 Pro is Razer’s most recent mouse to join the lightweight, sub-50g club. The mouse weighs just 49g or 1.72oz so it doesn’t feel like it’s weighing down your bag when you’re traveling with it. Because the mouse is so light, it requires less physical effort to move, stop and change direction. That also means your hand and wrist don’t feel fatigued during long gaming sessions, making the Viper V4 Pro a great choice for competitive players.</p><p>I’ve tested plenty of mice in my time, and the Viper V4 Pro is one of the lightest rodents I’ve used. It’s even lighter than the <a href="https://www.tomsguide.com/computing/mice/logitech-g-pro-x2-superstrike-review">Logitech G Pro X2 Superstrike</a> ($179, 2.15oz), our top pick for the <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a>. The Viper V4 Pro trumps other ultra-lightweight mice too, such as the <a href="https://www.tomsguide.com/computing/mice/keychron-m3-mini-8k-review">Keychron M3 Mini V2 8K</a> ($69, 1.94oz) and the <a href="https://www.tomsguide.com/computing/peripherals/nzxt-lift-elite-wireless-gaming-mouse-review">NZXT Lift Elite Wireless</a> ($79, 2.01oz).</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="5T9WQojfvaNJzVoS2t6XUM" name="Razer_Viper_V4_Pro_Mouse_011.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/5T9WQojfvaNJzVoS2t6XUM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 just because the Viper V4 Pro is as light as a feather doesn’t mean it isn’t sturdy. Made of high-quality plastic, the Viper V4 Pro feels grippy to hold straight out of the box. Razer happily includes anti-slip grip tape in the box that you can stick to the mouse for more grip. The rodent measures 5 x 2.51 x 1.57 inches and feels substantial to hold for those with medium-sized hands — like I do. Both claw and palm grips work with the Viper V4 Pro, too, making it a versatile pick.</p><h2 id="industry-leading-specs">Industry-leading specs</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="627gTmqh2XLbYRfaoL2TTM" name="Razer_Viper_V4_Pro_Mouse_003.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/627gTmqh2XLbYRfaoL2TTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>How much is too much DPI? Certainly not 50,000, according to Razer. That’s right: the Razer Viper V4 Pro boasts a maximum DPI of 50,000. That’s a <em>lot</em>, and at the time of writing, it’s industry-leading. We haven’t tested any other mice that beat the Viper V4 Pro. The Logitech G Pro X2 Superstrike comes close with its 44,000 DPI, though. I’d say both are overkill because who <em>really </em>needs <em>that </em>much speed? It’s there if you do, of course.</p><p>Changing the DPI is extremely easy. The Viper V4 Pro has a DPI button located on its underside, and by pressing it, you can cycle through 400, 800, 1600, 3200 and 6400 DPI by default. These can be customized via Razer Synapse (more on that shortly). I appreciate being able to adjust the DPI on the fly, as not being able to do so was one of my biggest gripes with the <a href="https://www.tomsguide.com/gaming/peripherals/logitech-g-pro-x-superlight-2-review">Logitech G Pro X Superlight 2</a> ($159).</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="txJb4CxAEFFrcTQ7yQ2JTM" name="Razer_Viper_V4_Pro_Mouse_009.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/txJb4CxAEFFrcTQ7yQ2JTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Aiding the Viper V4 Pro’s gaming prowess is its 8,000Hz polling rate which you can get over both wired and 2.4GHz wireless connections. The mouse is accompanied by a dome-shaped dongle which needs to be plugged into your machine with a USB-C cable. It’s fitted with three LED indicators which are color-coded to indicate the polling rate, connection stability, and battery level. There’s no Bluetooth but the lack of it on an esports mouse isn’t a major drawback.</p><h2 id="phenomenal-gaming-performance">Phenomenal gaming 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="sa62sd6TVMRtES8h4z6ZUM" name="Razer_Viper_V4_Pro_Mouse_010.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/sa62sd6TVMRtES8h4z6ZUM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="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">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>In theory, the Razer Viper V4 Pro sounds incredible, but does it perform as well in real-world use? Hell yeah, it does. I’ll preface this by saying that I’m not a professional esports player, but even I felt like the Viper V4 Pro gave me a competitive advantage in every game I played. Counter-Strike 2? Walk in the park. Cyberpunk 2077? Easy as pie. The Viper V4 Pro never once let me down throughout my testing.</p><p>I kicked things off by playing a few rounds of Counter-Strike 2. Admittedly, I’ve never been great at the game, but the Viper V4 Pro ensured I performed to the best of my ability. 8,000Hz polling meant that I didn’t experience any latency issues and all of my commands were instantaneously registered.</p><p>What I love about the Viper V4 Pro is its adaptive polling rate which, once enabled through Razer Synapse, boosts the polling up to 8KHz when gaming and drops it back to 1KHz when you’re doing non-gaming things on your computer. This helps preserve the mouse’s 180-hour battery, and it worked as intended in my 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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="xYMH9uwVg3icpQUkK4AfSM" name="Razer_Viper_V4_Pro_Mouse_005.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/xYMH9uwVg3icpQUkK4AfSM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>So, how about that 50,000 DPI, eh? It is the headline feature, after all. I can’t see myself using such high DPI but it might be beneficial for some. Personally, I find DPI around the 10,000 mark more than sufficient for snappy movements. In Counter-Strike 2 and Cyberpunk 2077, anytime I was engaging in close-quarters combat, I upped the DPI to 10,000 (after assigning it via Razer Synapse). This made my movements snappy and rapid, and I was able to quickly cut down big groups of enemies with my knife.</p><p>Being able to change the DPI on the fly came in clutch when I went from heated combat to stealthy gameplay. Infiltrating heavily-guarded bases in Cyberpunk 2077 and Hitman felt like a walk in the park, thanks to a low DPI of 500 enabling me to execute precise headshots with my silenced pistol. It was especially useful when aiming at just one target surrounded by several other NPCs. Similarly, using a long-range sniper rifle to do the same felt effortless.</p><p>Last but not least, the Viper V4 Pro’s optical switches feel amazing to press. Razer says they require 12% lighter actuation compared to the ones you’ll find in the <a href="https://www.tomsguide.com/gaming/gaming-peripherals/razers-new-viper-v3-pro-gaming-mouse-boasts-an-obscene-8000hz-polling-rate">Viper V3 Pro</a> ($149) so they’re easier (and less fatiguing) to activate or spam-click. I love how much tactile feedback they provide as well.</p><h2 id="long-battery-life">Long battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="S3iD8qufmtaS2S6gz4kKTM" name="Razer_Viper_V4_Pro_Mouse_006.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/S3iD8qufmtaS2S6gz4kKTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 cherry on top of the cake is the Razer Viper V4 Pro’s long battery life. Razer says that the mouse can last 180 hours (1,000Hz polling) or 45 hours (8,000Hz polling). Because 8,000Hz polling is a drain on not just system resources but also the mouse, adaptive polling rate is highly beneficial as it helps make the Viper V4 Pro last as long as possible.</p><p>Thanks to this, the Viper V4 Pro upstages its competitors. The Logitech G Pro X2 Superstrike is rated for 90 hours; the Logitech G Pro X Superlight 2 is rated for 95 hours; and the Keychron M3 Mini V2 8K can last up to 140 hours. I couldn’t kill the Viper V4 Pro entirely in my testing, but after three hours of gaming with 8KHz polling enabled and another three working over 1,000Hz, the battery had dipped by only 8%. If it’s long battery life you’re after, the Viper V4 Pro is the way to go.</p><h2 id="user-friendly-companion-software-2">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/NPK6DL7v3TumPmUkiUaZsD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/6pEkWogjESw6xQyD5nvSpD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/Ht4YkFXpitvabNJuFEuTkD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/qYnzKtjJ5Lra3KaADCPodD-1920-80.jpg" alt="Razer Viper V4 Pro customization through Razer Synapse 4" /><figcaption><small role="credit">Razer / Tom's Guide</small></figcaption></figure></figure><p>To customize the Razer Viper V4 Pro, you’ll need to download <a href="https://www.razer.com/gb-en/synapse-4" target="_blank" rel="nofollow">Razer Synapse 4</a> on either Windows or macOS. It’s your one-stop shop for personalizing most Razer peripherals. The software sports a clean interface and is easy to navigate.</p><p>It allows you to assign different DPI values to the Viper V4 Pro’s DPI button, adjust the polling rate and toggle adaptive polling, remap the buttons, and customize lift-off distance. It’s worth taking a few minutes and familiarizing yourself with the different settings so that you can take advantage of everything the Viper V4 Pro has to offer.</p><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-the-downs"><span>Razer Viper V4 Pro review: The downs</span></h2><p>There isn’t a lot wrong with the Razer Viper V4 Pro, but its loud clicks and steep price may not be for everyone.</p><h2 id="loud-clicks-won-t-be-for-everyone">Loud clicks won’t be for everyone</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Tno2e3NhjuKSTXrJim3ySM" name="Razer_Viper_V4_Pro_Mouse_008.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/Tno2e3NhjuKSTXrJim3ySM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 second thing I noticed about the Razer Viper V4 Pro after its lightweight design was the sound its left- and right-buttons make when pressed. And <em>boy</em>, are they loud. My colleagues instantly noticed it too, and said that the loud sound wasn’t their cup of tea.</p><p>This is all to say that if you prefer a quiet mouse, like the SteelSeries Aerox 3 Wireless Gen 2, the Viper V4 Pro might not be for you — although given how powerful it is, it’s a compromise I’d be willing to make.</p><h2 id="expensive-but-worth-it-for-esports-players">Expensive (but worth it for esports players)</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Gw3GmEp7AXDUUwT9qQZrTM" name="Razer_Viper_V4_Pro_Mouse_012.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/Gw3GmEp7AXDUUwT9qQZrTM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 more of a pre-purchase consideration rather than an outright flaw: the Razer Viper V4 Pro isn’t for the budget conscious. The mouse retails for <a href="https://www.amazon.com/Razer-Viper-Wireless-Esports-Gaming/dp/B0GMLBSSTD/" target="_blank" rel="nofollow">$159</a> / <a href="https://www.amazon.co.uk/Razer-Viper-Pro-sports-Ultra-lightweight-Black/dp/B0G5QBTC2Q/" target="_blank" rel="nofollow">£159 at Amazon</a> so it’s on the pricey side of things. Esports mice, however, are usually expensive and the Viper V4 Pro isn’t beyond the pale here.</p><p>The Viper V4 Pro costs the same as the Logitech G Pro X Superlight 2 ($159) and is slightly cheaper than the Logitech G Pro X2 Superstrike ($179). But if you aren’t fussed about 50,000 DPI, there are plenty of sub-$100 alternatives available. The Keychron M3 Mini V2 8K ($69) packs 8,000Hz polling and 30,000 DPI; the NZXT Lift Elite Wireless ($79) maxes out at 26,000 DPI; and the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69) combines ergonomic benefits with powerful gaming features.</p><p>Unless you’re a competitive and professional esports player, there’s little reason to splash out on the Viper V4 Pro. But if you want a mouse that’s at the top of its game, few are better than Razer’s flagship model.</p><h2 class="article-body__section" id="section-razer-viper-v4-pro-review-verdict"><span>Razer Viper V4 Pro 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="qReRy9buAtnk7CSXmCZHSM" name="Razer_Viper_V4_Pro_Mouse_002.JPG" alt="A black Razer Viper V4 Pro mouse" src="https://cdn.mos.cms.futurecdn.net/qReRy9buAtnk7CSXmCZHSM-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 can’t recall the last time I had so much fun testing a mouse. The Razer Viper V4 Pro sets a new gold standard for gaming mice with its 50,000 DPI and 8,000Hz polling rate. It’s excellent for FPS titles as well as RPGs, and class-leading battery life goes a long way to justify its premium price tag.</p><p>The Viper V4 Pro is extremely light and comfortable too, and it sports a low-key, minimalist design that doesn’t draw eyeballs to your desk… though the loud clicks might. Its optical switches provide great tactile feedback when pressed, and user-friendly companion software makes customization quick and easy.</p><p>Though casual gamers are unlikely to benefit from features like the 50,000 DPI, competitive gamers looking for a fast, light and capable mouse should look no further than the Viper V4 Pro. It’s simply phenomenal.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The IQUNIX MQ80 is one of the loveliest mechanical keyboards I’ve tested so far — here’s my honest verdict on whether it’s worth $169 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The IQUNIX MQ80 is a lovely mechanical keyboard that’s an utter delight to type on. Its Kailh Gold Red linear switches are responsive and they sound deliciously creamy. As a low-profile keyboard, it’s comfortable and non-fatiguing to type on for hours on end. For casual gamers, there’s 1,000Hz polling too.</p><p>It isn’t perfect, though. As the MQ80’s case is made of aluminum, its height isn’t adjustable. There are no physical toggles on it either, which means you need to rely on finicky key combos for changing settings. However, neither of these drawbacks keep it from earning a spot amongst the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> today.</p><p>But do you have nearly $170 to splash out? If yes, then this could be the keyboard for you. For the complete breakdown, read my full IQUNIX MQ80 review.</p><h2 class="article-body__section" id="section-iqunix-mq80-review-specs"><span>IQUNIX MQ80 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/IQUNIX-MQ80-Low-Profile-Keyboard/dp/B0F8NFTC9F/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/iQunix-MQ80-Low-Profile-Keyboard/dp/B0F8NDVHJW/" target="_blank" rel="nofollow">£180</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Kailh Gold Red (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Le-tray gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>80%, 84 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz); 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>4,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.64 x 4.9 x 1.08 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.6lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Shadow Black, Moonlight Silver</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-iqunix-mq80-review-cheat-sheet"><span>IQUNIX MQ80 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A low-profile mechanical keyboard sporting an 80% layout</li><li><strong>Who is it for? </strong>Hardcore typists who need a comfortable keyboard with responsive switches</li><li><strong>How much does it cost? </strong>The IQUNIX MQ80 is available for <a href="https://www.amazon.com/IQUNIX-MQ80-Low-Profile-Keyboard/dp/B0F8NFTC9F/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/iQunix-MQ80-Low-Profile-Keyboard/dp/B0F8NDVHJW/" target="_blank" rel="nofollow">£180</a></li><li><strong>What do we like? </strong>The stellar aluminum build, beautiful typing experience and sound profile, low-profile design, and the web-based software</li><li><strong>What don’t we like?</strong> The frustrating key combos, steep price point, and non-adjustable height</li></ul><h2 class="article-body__section" id="section-iqunix-mq80-review-the-ups"><span>IQUNIX MQ80 review: The ups</span></h2><p>From its sturdy build quality and comfortable low-profile design, to its outstanding typing experience and creamy sound profile, the IQUNIX MQ80 is a winner.</p><h2 id="stellar-build-quality">Stellar build 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="7f5SrYfbTaRcxUJpNZVm3a" name="IQUNIX_MQ80_Mechanical_keyboard_003.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/7f5SrYfbTaRcxUJpNZVm3a-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>IQUNIX is yet to let me down with its keyboards’ build quality. Both the <a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">IQUNIX Magi75</a> ($169) and the <a href="https://www.tomsguide.com/computing/keyboards/iquinix-magi65-pro-review">IQUNIX Magi65 Pro</a> ($139) are built exceptionally well, and the IQUNIX MQ80 is no different. Similar to the Magi75, the MQ80 utilizes a CNC machined aluminum chassis as well as top-notch plastic to deliver a sturdy and robust keyboard.</p><p>The aluminum used naturally gives it a bit of heft as the MQ80 weighs 2.6lbs so it’s best suited to staying put on your desk. It’s substantially heavier than the Magi75 which weighs 2.09lbs, and other plastic keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129, 1.67lbs) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139, 1.65lbs). But an advantage of buying the MQ80 over plastic keyboards is that the MQ80’s aluminum case is scratch-resistant which aids longevity.</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="ZtgGGtxyqiFozv9heuaahY" name="IQUNIX_MQ80_Mechanical_keyboard_010.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/ZtgGGtxyqiFozv9heuaahY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Because the MQ80 is enclosed in an aluminum case, there’s no flex in the body, even when you try to twist it by holding its side. This is an all-too-common occurrence with plastic keyboards but, for obvious reasons, not with aluminum ones. The MQ80 also utilizes double-shot PBT keycaps with an anti-oil coating, and they’re more resistant to wear and tear when compared to their ABS counterparts.</p><p>Also similar to the Magi75, the MQ80 features an le-tray gasket mount which features four PCB support arms and silicone rings, thereby aiding the keyboard’s structural integrity. According to IQUNIX, the le-tray gasket mount ensures a smooth, soft landing with every keystroke, “unlike other low-profile keyboards that bottom out against a hard plastic surface.” This, essentially, ensures a soft, cushioned typing feel (that you get with other gasket mounts), as well as stability for gaming (as you get with tray mounts).</p><p>Several layers of sound-dampening foam have also been added to the MQ80’s internal construction. Combined with the mount type, these give the keyboard a creamy sound profile (more on that soon).</p><h2 id="comfortable-low-profile-design">Comfortable low-profile 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="ufnUsmwtCsHuDBokKMwJQa" name="IQUNIX_MQ80_Mechanical_keyboard_005.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/ufnUsmwtCsHuDBokKMwJQa-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 after a keyboard that has ergonomic advantages but isn’t a split deck, a low-profile one is a good choice. The IQUNIX MQ80 is a low-profile keyboard that sits close to the surface it’s on, so your wrists remain in a more neutral, straight position. Wrist pain in typists is usually caused by angling your wrists upwards — a low-profile keyboard like the MQ80 eliminates that, enabling you to type for longer without feeling discomfort.</p><p>Given the keyboard’s low-profile design, the MQ80 utilizes low-profile Kailh Gold Red switches. Compared to standard-profile switches, low-profile ones are easier to activate, thanks to their reduced actuation distance. The Gold Reds require just 40g of force to activate with a pre-travel distance of 1.2mm (2.8mm before bottoming out), and as you’ll soon see, they’re extremely fun and comfortable to type on.</p><p>All in all, I’ve found the MQ80 extremely comfy to type on — I even typed this full review (and my <a href="https://www.tomsguide.com/cameras-photography/drones/potensic-atom-3-review">Potensic Atom 3 drone review</a>) on it!</p><h2 id="beautiful-to-type-on-and-listen-to">Beautiful to type on and listen to</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="KZZTDMEYQtBqFT8ffXiubY" name="IQUNIX_MQ80_Mechanical_keyboard_012.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/KZZTDMEYQtBqFT8ffXiubY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 IQUNIX MQ80 offers a typing experience that’s nothing short of heavenly. If, like me, you type for a living, you’ll know the importance of a responsive and comfortable keyboard — and the MQ80 is just that. It comes fitted with pre-lubed Kailh POM Gold Red linear mechanical switches which are simply delicious.</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="53Ux7NgSAmxYYxGCfo4ceY" name="IQUNIX_MQ80_Mechanical_keyboard_009.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/53Ux7NgSAmxYYxGCfo4ceY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>These low-profile switches are buttery smooth to type on, and throughout the testing period, I feel like my fingers have been gliding across the MQ80. As I mentioned earlier, they require just 40g of force to activate. They’re lighter than the 50g Kailh White Rains in the Hexgears Immersion A3 TKL (the keyboard I use when I work from home) and only ever-so slightly heavier (negligible, really) than the 38g Wisteria V2s in the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> (which I use in the office).</p><p>The MQ80 sounds delightful too. Its sound profile can be best described as creamy, and if you’re wondering whether you should get the Magi75 or the MQ80, this could be the decider. I’d say the Magi75 sounds slightly more thocky while the MQ80 sounds more silky. I know I’d pick the latter.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>IQUNIX MQ80</p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-kw-300-mx-review">Cherry KW 300 MX</a></p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">IQUNIX Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.75</p></td><td  ><p>91.23%</p></td></tr></tbody></table></div><p>Now that I’m done waxing lyrical about the MQ80’s typing experience, how does it compare to other keyboards I’ve recently tested? It performs well, as you can see from the table above. Straight out of the box, I was able to achieve 95 words per minute with 92.8% accuracy. A little lower than what I achieved on the Epomaker Glyph, but I’m a happy camper any time I get over 90WPM on a keyboard.</p><h2 id="it-ll-game-too-2">It’ll game, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9XM5to28GhL2s6Li8ZqUYa" name="IQUNIX_MQ80_Mechanical_keyboard_004.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/9XM5to28GhL2s6Li8ZqUYa-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="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">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>Though the IQUNIX MQ80 is more of a productivity keyboard, it’s got a 1,000Hz polling rate for anyone who wants to play a couple rounds of Counter-Strike 2 or a few hours of <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> in their downtime. 1,000Hz polling is available over a wired and 2.4GHz connection only, and it naturally drops to 125Hz over Bluetooth.</p><p>I tested the MQ80 by playing the aforementioned games, as well as a quick lap around Silverstone in F1 25. I didn’t notice any latency issues, and all my commands were registered instantaneously. Since the linear switches don’t require a lot of force to activate and they have a short travel distance, I was able to simply tap the keys when I needed to quickly execute an action — like ducking after throwing a grenade.</p><p>Of course, if you’re a competitive gamer, I’d recommend grabbing a Hall Effect keyboard instead. The <a href="https://www.tomsguide.com/computing/peripherals/keychron-k4-he-keyboard-review">Keychron K4 HE</a> ($134) with its per- and quad-key actuation is a superb choice. There’s also the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a> ($99) with its 8,000Hz polling rate.</p><h2 id="web-based-companion-software-2">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/nVorDzSZQWfQxEVNP4qpxN-1920-80.jpg" alt="Screenshots of VIA" /><figcaption><small role="credit">VIA / IQUNIX / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/CWUhiwJuHKbQ4RGgTECUxN-1920-80.jpg" alt="Screenshots of VIA" /><figcaption><small role="credit">VIA / IQUNIX / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/PnsaxGB227PKTCS9dWP42P-1920-80.jpg" alt="Screenshots of VIA" /><figcaption><small role="credit">VIA / IQUNIX / Tom's Guide</small></figcaption></figure></figure><p>I love it when keyboards — and other gadgets — can be customized via web-based software. It saves me downloading bloatware or proprietary software onto my laptop. The IQUNIX MQ80 runs open source QMK firmware and is customizable using <a href="https://www.usevia.app/" target="_blank" rel="nofollow">VIA</a>, a web-based app.</p><p>You’ll need to <a href="https://iqunix.com/pages/iqunix-super-custom-keyboard-kit-user-guide" target="_blank" rel="nofollow">download the MQ80’s JSON file</a> and upload it to VIA through the Design tab. Once that’s done, plug in the MQ80 and you’re good to go. The app lets you remap keys, record macros, and adjust the RGB lighting’s effects and colors. VIA is easy to get a hang of, thanks to its clean, user-friendly interface. It doesn’t offer the most in-depth customization around, but that’s not as important on a mechanical deck as it is on a magnetic one — VIA does the job it needs to reliably enough.</p><h2 class="article-body__section" id="section-iqunix-mq80-review-the-downs"><span>IQUNIX MQ80 review: The downs</span></h2><p>There isn’t a lot wrong with the IQUNIX MQ80, but the keyboard’s key combos are frustrating and the height isn’t adjustable.</p><h2 id="frustrating-key-combos-for-settings">Frustrating key combos for settings</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YCULokNM2EyJ8vSBHFLxpZ" name="IQUNIX_MQ80_Mechanical_keyboard_015.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/YCULokNM2EyJ8vSBHFLxpZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 tested the IQUNIX Magi75, I found the lack of physical toggles to change settings annoying. That annoyance remains even with the IQUNIX MQ80 as the key combos required to change any of its settings are frustrating, at best. It’s good of IQUNIX to include a physical manual in the box (and a <a href="https://iqunix.com/blogs/news/mq80-user-guide?srsltid=AfmBOoqm-_lewvu9yqc41vfsIXDum1-cVVHb7tDIE39xqyZ9jMyFuI3G" target="_blank" rel="nofollow">digital copy on its website</a> too), but did the keyboard have to be <em>this </em>slow to register the combos?</p><p>Similar to the Magi75, it took me nearly 15 minutes to connect the MQ80 to my MacBook Air M2. For some reason, plugging the keyboard via the included USB-C cable didn’t work and only worked once I’d established a Bluetooth connection, and disconnected said wireless connection while keeping the keyboard plugged in.</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="gs36HTfoHWCQY3iz2YxaRY" name="IQUNIX_MQ80_Mechanical_keyboard_006.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gs36HTfoHWCQY3iz2YxaRY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 pairing the MQ80 to my laptop was no mean feat. When I followed the instructions to put the keyboard into Bluetooth pairing mode — Fn + 1/2/3 — it took several minutes for the MQ80 to connect to my laptop. I thought it was a problem with my MacBook so I tried connecting the MQ80 to the <a href="https://www.tomsguide.com/computing/laptops/geekom-geekbook-x14-pro-review">Geekom GeekBook X14 Pro</a>, and the same issue persisted.</p><p>Fitting physical toggles on an aluminum keyboard isn’t <em>impossible, </em>as all it requires is thinning out an area of the case’s internals, drilling a hole in it, and screwing on a toggle switch to the PCB. The likes of Keychron and NuPhy have already done it, and there’s no excuse for IQUNIX to not do the same.</p><p>In addition to that, the MQ80’s key combinations are confusing, taking ages to register, and are time-consuming. I said it before and I’ll say it again: I really hope IQUNIX fixes this with future releases, because it negatively impacts the user experience.</p><h2 id="non-adjustable-height">Non-adjustable height</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jfYCezPuAR2nsWX3VPkEYY" name="IQUNIX_MQ80_Mechanical_keyboard_007.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/jfYCezPuAR2nsWX3VPkEYY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 isn’t a drawback per se but more of a pre-purchase consideration: the IQUNIX MQ80 doesn’t have feet on its underside so its height can’t be adjusted. This is an issue with most aluminum keyboards, as adding feet makes machining and manufacturing trickier. The Lofree Flow 2, however, <em>does </em>have adjustable feet, despite being made of aluminum — so it isn’t impossible to do, and it’d be nice to see more manufacturers do the same.</p><p>Other keyboards that lack adjustable height include the Epomaker Luma84 and the Keychron Q3 Ultra 8K, both of which are also made of aluminum. You’ll need to sacrifice the metal build if you want adjustable feet, and if you’re happy to do so, the Hexgears Immersion A3 TKL, the NuPhy Air75 V3, and the NuPhy Node75. All are excellent low-profile alternatives, and are made of high-quality plastic.</p><h2 id="expensive-2">Expensive</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PxkuHVVTn75TYBjUZnpn5a" name="IQUNIX_MQ80_Mechanical_keyboard_014.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/PxkuHVVTn75TYBjUZnpn5a-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the IQUNIX MQ80’s price may have you considering other options. The MQ80 retails for <a href="https://www.amazon.com/IQUNIX-MQ80-Low-Profile-Keyboard/dp/B0F8NFTC9F/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/iQunix-MQ80-Low-Profile-Keyboard/dp/B0F8NDVHJW/" target="_blank" rel="nofollow">£180 at Amazon</a>. Don’t get me wrong: the MQ80 is a super fun keyboard to type on, and it’s built well too, but does it justify its price? I’m not entirely convinced, especially as it enters an already crowded market without really offering anything unique that you won’t find elsewhere.</p><p>Keyboard manufacturers like Keychron, Epomaker, and NuPhy dominate the market with decks that don’t break the bank (as well as some that, admittedly, do). If you don’t want to spend just under $170 on the MQ80, don’t fret; there are plenty of cheaper alternatives available. I’d recommend the Lofree Flow Lite84 ($109), the Epomaker Luma84 ($109), the NuPhy Air75 V3 ($139) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-k8-max-review">Keychron K8 Max</a> ($124) are all outstanding and have either mine or my team’s stamp of approval.</p><h2 class="article-body__section" id="section-iqunix-mq80-review-verdict"><span>IQUNIX MQ80 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="3Uxg9erxqvA4ZpZdQEzMNa" name="IQUNIX_MQ80_Mechanical_keyboard_002.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/3Uxg9erxqvA4ZpZdQEzMNa-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 type for a living and need a comfortable, low-profile keyboard to do so, the IQUNIX MQ80 is an excellent choice. Featuring Kailh Gold Red linear switches that are highly responsive, the MQ80 sounds wonderful too. It’s built extremely well, thanks to its aluminum case, and web-based VIA makes customization straightforward. Oh, and it’s got 1,000Hz polling for casual gaming.</p><p>Few things in life are perfect, though, and the MQ80 isn’t one of those. Its key combos are annoying and slow to register, and the keyboard’s height isn’t adjustable. That $169 price tag may be hard to stomach for some too.</p><p>But if you don’t mind any of those flaws, and all you care about is a beautiful typing experience, then you can’t go wrong with the MQ80.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/iqunix-mq80-review</link>
                                                                            <description>
                            <![CDATA[ The IQUNIX MQ80 is a stellar mechanical keyboard. It's a joy to type on and listen to, and its web-based companion software makes customization easy. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">K8Ba7XaW4RALQcfvyD5nVS</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/6U5887ydo7qhX3cA7Fe7Sa-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 06 Jul 2026 08:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/6U5887ydo7qhX3cA7Fe7Sa-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black IQUNIX MQ80 mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A black IQUNIX MQ80 mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A black IQUNIX MQ80 mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/6U5887ydo7qhX3cA7Fe7Sa-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The IQUNIX MQ80 is a lovely mechanical keyboard that’s an utter delight to type on. Its Kailh Gold Red linear switches are responsive and they sound deliciously creamy. As a low-profile keyboard, it’s comfortable and non-fatiguing to type on for hours on end. For casual gamers, there’s 1,000Hz polling too.</p><p>It isn’t perfect, though. As the MQ80’s case is made of aluminum, its height isn’t adjustable. There are no physical toggles on it either, which means you need to rely on finicky key combos for changing settings. However, neither of these drawbacks keep it from earning a spot amongst the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> today.</p><p>But do you have nearly $170 to splash out? If yes, then this could be the keyboard for you. For the complete breakdown, read my full IQUNIX MQ80 review.</p><h2 class="article-body__section" id="section-iqunix-mq80-review-specs"><span>IQUNIX MQ80 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/IQUNIX-MQ80-Low-Profile-Keyboard/dp/B0F8NFTC9F/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/iQunix-MQ80-Low-Profile-Keyboard/dp/B0F8NDVHJW/" target="_blank" rel="nofollow">£180</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Kailh Gold Red (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Le-tray gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>80%, 84 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz); 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>4,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.64 x 4.9 x 1.08 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.6lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Shadow Black, Moonlight Silver</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-iqunix-mq80-review-cheat-sheet"><span>IQUNIX MQ80 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A low-profile mechanical keyboard sporting an 80% layout</li><li><strong>Who is it for? </strong>Hardcore typists who need a comfortable keyboard with responsive switches</li><li><strong>How much does it cost? </strong>The IQUNIX MQ80 is available for <a href="https://www.amazon.com/IQUNIX-MQ80-Low-Profile-Keyboard/dp/B0F8NFTC9F/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/iQunix-MQ80-Low-Profile-Keyboard/dp/B0F8NDVHJW/" target="_blank" rel="nofollow">£180</a></li><li><strong>What do we like? </strong>The stellar aluminum build, beautiful typing experience and sound profile, low-profile design, and the web-based software</li><li><strong>What don’t we like?</strong> The frustrating key combos, steep price point, and non-adjustable height</li></ul><h2 class="article-body__section" id="section-iqunix-mq80-review-the-ups"><span>IQUNIX MQ80 review: The ups</span></h2><p>From its sturdy build quality and comfortable low-profile design, to its outstanding typing experience and creamy sound profile, the IQUNIX MQ80 is a winner.</p><h2 id="stellar-build-quality">Stellar build 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="7f5SrYfbTaRcxUJpNZVm3a" name="IQUNIX_MQ80_Mechanical_keyboard_003.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/7f5SrYfbTaRcxUJpNZVm3a-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>IQUNIX is yet to let me down with its keyboards’ build quality. Both the <a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">IQUNIX Magi75</a> ($169) and the <a href="https://www.tomsguide.com/computing/keyboards/iquinix-magi65-pro-review">IQUNIX Magi65 Pro</a> ($139) are built exceptionally well, and the IQUNIX MQ80 is no different. Similar to the Magi75, the MQ80 utilizes a CNC machined aluminum chassis as well as top-notch plastic to deliver a sturdy and robust keyboard.</p><p>The aluminum used naturally gives it a bit of heft as the MQ80 weighs 2.6lbs so it’s best suited to staying put on your desk. It’s substantially heavier than the Magi75 which weighs 2.09lbs, and other plastic keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129, 1.67lbs) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139, 1.65lbs). But an advantage of buying the MQ80 over plastic keyboards is that the MQ80’s aluminum case is scratch-resistant which aids longevity.</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="ZtgGGtxyqiFozv9heuaahY" name="IQUNIX_MQ80_Mechanical_keyboard_010.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/ZtgGGtxyqiFozv9heuaahY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Because the MQ80 is enclosed in an aluminum case, there’s no flex in the body, even when you try to twist it by holding its side. This is an all-too-common occurrence with plastic keyboards but, for obvious reasons, not with aluminum ones. The MQ80 also utilizes double-shot PBT keycaps with an anti-oil coating, and they’re more resistant to wear and tear when compared to their ABS counterparts.</p><p>Also similar to the Magi75, the MQ80 features an le-tray gasket mount which features four PCB support arms and silicone rings, thereby aiding the keyboard’s structural integrity. According to IQUNIX, the le-tray gasket mount ensures a smooth, soft landing with every keystroke, “unlike other low-profile keyboards that bottom out against a hard plastic surface.” This, essentially, ensures a soft, cushioned typing feel (that you get with other gasket mounts), as well as stability for gaming (as you get with tray mounts).</p><p>Several layers of sound-dampening foam have also been added to the MQ80’s internal construction. Combined with the mount type, these give the keyboard a creamy sound profile (more on that soon).</p><h2 id="comfortable-low-profile-design">Comfortable low-profile 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="ufnUsmwtCsHuDBokKMwJQa" name="IQUNIX_MQ80_Mechanical_keyboard_005.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/ufnUsmwtCsHuDBokKMwJQa-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 after a keyboard that has ergonomic advantages but isn’t a split deck, a low-profile one is a good choice. The IQUNIX MQ80 is a low-profile keyboard that sits close to the surface it’s on, so your wrists remain in a more neutral, straight position. Wrist pain in typists is usually caused by angling your wrists upwards — a low-profile keyboard like the MQ80 eliminates that, enabling you to type for longer without feeling discomfort.</p><p>Given the keyboard’s low-profile design, the MQ80 utilizes low-profile Kailh Gold Red switches. Compared to standard-profile switches, low-profile ones are easier to activate, thanks to their reduced actuation distance. The Gold Reds require just 40g of force to activate with a pre-travel distance of 1.2mm (2.8mm before bottoming out), and as you’ll soon see, they’re extremely fun and comfortable to type on.</p><p>All in all, I’ve found the MQ80 extremely comfy to type on — I even typed this full review (and my <a href="https://www.tomsguide.com/cameras-photography/drones/potensic-atom-3-review">Potensic Atom 3 drone review</a>) on it!</p><h2 id="beautiful-to-type-on-and-listen-to">Beautiful to type on and listen to</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="KZZTDMEYQtBqFT8ffXiubY" name="IQUNIX_MQ80_Mechanical_keyboard_012.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/KZZTDMEYQtBqFT8ffXiubY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 IQUNIX MQ80 offers a typing experience that’s nothing short of heavenly. If, like me, you type for a living, you’ll know the importance of a responsive and comfortable keyboard — and the MQ80 is just that. It comes fitted with pre-lubed Kailh POM Gold Red linear mechanical switches which are simply delicious.</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="53Ux7NgSAmxYYxGCfo4ceY" name="IQUNIX_MQ80_Mechanical_keyboard_009.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/53Ux7NgSAmxYYxGCfo4ceY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>These low-profile switches are buttery smooth to type on, and throughout the testing period, I feel like my fingers have been gliding across the MQ80. As I mentioned earlier, they require just 40g of force to activate. They’re lighter than the 50g Kailh White Rains in the Hexgears Immersion A3 TKL (the keyboard I use when I work from home) and only ever-so slightly heavier (negligible, really) than the 38g Wisteria V2s in the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a> (which I use in the office).</p><p>The MQ80 sounds delightful too. Its sound profile can be best described as creamy, and if you’re wondering whether you should get the Magi75 or the MQ80, this could be the decider. I’d say the Magi75 sounds slightly more thocky while the MQ80 sounds more silky. I know I’d pick the latter.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>IQUNIX MQ80</p></td><td  ><p>95</p></td><td  ><p>92.8%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a></p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-kw-300-mx-review">Cherry KW 300 MX</a></p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">IQUNIX Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.75</p></td><td  ><p>91.23%</p></td></tr></tbody></table></div><p>Now that I’m done waxing lyrical about the MQ80’s typing experience, how does it compare to other keyboards I’ve recently tested? It performs well, as you can see from the table above. Straight out of the box, I was able to achieve 95 words per minute with 92.8% accuracy. A little lower than what I achieved on the Epomaker Glyph, but I’m a happy camper any time I get over 90WPM on a keyboard.</p><h2 id="it-ll-game-too-2">It’ll game, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="9XM5to28GhL2s6Li8ZqUYa" name="IQUNIX_MQ80_Mechanical_keyboard_004.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/9XM5to28GhL2s6Li8ZqUYa-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="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">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>Though the IQUNIX MQ80 is more of a productivity keyboard, it’s got a 1,000Hz polling rate for anyone who wants to play a couple rounds of Counter-Strike 2 or a few hours of <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> in their downtime. 1,000Hz polling is available over a wired and 2.4GHz connection only, and it naturally drops to 125Hz over Bluetooth.</p><p>I tested the MQ80 by playing the aforementioned games, as well as a quick lap around Silverstone in F1 25. I didn’t notice any latency issues, and all my commands were registered instantaneously. Since the linear switches don’t require a lot of force to activate and they have a short travel distance, I was able to simply tap the keys when I needed to quickly execute an action — like ducking after throwing a grenade.</p><p>Of course, if you’re a competitive gamer, I’d recommend grabbing a Hall Effect keyboard instead. The <a href="https://www.tomsguide.com/computing/peripherals/keychron-k4-he-keyboard-review">Keychron K4 HE</a> ($134) with its per- and quad-key actuation is a superb choice. There’s also the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a> ($99) with its 8,000Hz polling rate.</p><h2 id="web-based-companion-software-2">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/nVorDzSZQWfQxEVNP4qpxN-1920-80.jpg" alt="Screenshots of VIA" /><figcaption><small role="credit">VIA / IQUNIX / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/CWUhiwJuHKbQ4RGgTECUxN-1920-80.jpg" alt="Screenshots of VIA" /><figcaption><small role="credit">VIA / IQUNIX / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/PnsaxGB227PKTCS9dWP42P-1920-80.jpg" alt="Screenshots of VIA" /><figcaption><small role="credit">VIA / IQUNIX / Tom's Guide</small></figcaption></figure></figure><p>I love it when keyboards — and other gadgets — can be customized via web-based software. It saves me downloading bloatware or proprietary software onto my laptop. The IQUNIX MQ80 runs open source QMK firmware and is customizable using <a href="https://www.usevia.app/" target="_blank" rel="nofollow">VIA</a>, a web-based app.</p><p>You’ll need to <a href="https://iqunix.com/pages/iqunix-super-custom-keyboard-kit-user-guide" target="_blank" rel="nofollow">download the MQ80’s JSON file</a> and upload it to VIA through the Design tab. Once that’s done, plug in the MQ80 and you’re good to go. The app lets you remap keys, record macros, and adjust the RGB lighting’s effects and colors. VIA is easy to get a hang of, thanks to its clean, user-friendly interface. It doesn’t offer the most in-depth customization around, but that’s not as important on a mechanical deck as it is on a magnetic one — VIA does the job it needs to reliably enough.</p><h2 class="article-body__section" id="section-iqunix-mq80-review-the-downs"><span>IQUNIX MQ80 review: The downs</span></h2><p>There isn’t a lot wrong with the IQUNIX MQ80, but the keyboard’s key combos are frustrating and the height isn’t adjustable.</p><h2 id="frustrating-key-combos-for-settings">Frustrating key combos for settings</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YCULokNM2EyJ8vSBHFLxpZ" name="IQUNIX_MQ80_Mechanical_keyboard_015.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/YCULokNM2EyJ8vSBHFLxpZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 tested the IQUNIX Magi75, I found the lack of physical toggles to change settings annoying. That annoyance remains even with the IQUNIX MQ80 as the key combos required to change any of its settings are frustrating, at best. It’s good of IQUNIX to include a physical manual in the box (and a <a href="https://iqunix.com/blogs/news/mq80-user-guide?srsltid=AfmBOoqm-_lewvu9yqc41vfsIXDum1-cVVHb7tDIE39xqyZ9jMyFuI3G" target="_blank" rel="nofollow">digital copy on its website</a> too), but did the keyboard have to be <em>this </em>slow to register the combos?</p><p>Similar to the Magi75, it took me nearly 15 minutes to connect the MQ80 to my MacBook Air M2. For some reason, plugging the keyboard via the included USB-C cable didn’t work and only worked once I’d established a Bluetooth connection, and disconnected said wireless connection while keeping the keyboard plugged in.</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="gs36HTfoHWCQY3iz2YxaRY" name="IQUNIX_MQ80_Mechanical_keyboard_006.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gs36HTfoHWCQY3iz2YxaRY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 pairing the MQ80 to my laptop was no mean feat. When I followed the instructions to put the keyboard into Bluetooth pairing mode — Fn + 1/2/3 — it took several minutes for the MQ80 to connect to my laptop. I thought it was a problem with my MacBook so I tried connecting the MQ80 to the <a href="https://www.tomsguide.com/computing/laptops/geekom-geekbook-x14-pro-review">Geekom GeekBook X14 Pro</a>, and the same issue persisted.</p><p>Fitting physical toggles on an aluminum keyboard isn’t <em>impossible, </em>as all it requires is thinning out an area of the case’s internals, drilling a hole in it, and screwing on a toggle switch to the PCB. The likes of Keychron and NuPhy have already done it, and there’s no excuse for IQUNIX to not do the same.</p><p>In addition to that, the MQ80’s key combinations are confusing, taking ages to register, and are time-consuming. I said it before and I’ll say it again: I really hope IQUNIX fixes this with future releases, because it negatively impacts the user experience.</p><h2 id="non-adjustable-height">Non-adjustable height</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="jfYCezPuAR2nsWX3VPkEYY" name="IQUNIX_MQ80_Mechanical_keyboard_007.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/jfYCezPuAR2nsWX3VPkEYY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 isn’t a drawback per se but more of a pre-purchase consideration: the IQUNIX MQ80 doesn’t have feet on its underside so its height can’t be adjusted. This is an issue with most aluminum keyboards, as adding feet makes machining and manufacturing trickier. The Lofree Flow 2, however, <em>does </em>have adjustable feet, despite being made of aluminum — so it isn’t impossible to do, and it’d be nice to see more manufacturers do the same.</p><p>Other keyboards that lack adjustable height include the Epomaker Luma84 and the Keychron Q3 Ultra 8K, both of which are also made of aluminum. You’ll need to sacrifice the metal build if you want adjustable feet, and if you’re happy to do so, the Hexgears Immersion A3 TKL, the NuPhy Air75 V3, and the NuPhy Node75. All are excellent low-profile alternatives, and are made of high-quality plastic.</p><h2 id="expensive-2">Expensive</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="PxkuHVVTn75TYBjUZnpn5a" name="IQUNIX_MQ80_Mechanical_keyboard_014.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/PxkuHVVTn75TYBjUZnpn5a-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the IQUNIX MQ80’s price may have you considering other options. The MQ80 retails for <a href="https://www.amazon.com/IQUNIX-MQ80-Low-Profile-Keyboard/dp/B0F8NFTC9F/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/iQunix-MQ80-Low-Profile-Keyboard/dp/B0F8NDVHJW/" target="_blank" rel="nofollow">£180 at Amazon</a>. Don’t get me wrong: the MQ80 is a super fun keyboard to type on, and it’s built well too, but does it justify its price? I’m not entirely convinced, especially as it enters an already crowded market without really offering anything unique that you won’t find elsewhere.</p><p>Keyboard manufacturers like Keychron, Epomaker, and NuPhy dominate the market with decks that don’t break the bank (as well as some that, admittedly, do). If you don’t want to spend just under $170 on the MQ80, don’t fret; there are plenty of cheaper alternatives available. I’d recommend the Lofree Flow Lite84 ($109), the Epomaker Luma84 ($109), the NuPhy Air75 V3 ($139) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-k8-max-review">Keychron K8 Max</a> ($124) are all outstanding and have either mine or my team’s stamp of approval.</p><h2 class="article-body__section" id="section-iqunix-mq80-review-verdict"><span>IQUNIX MQ80 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="3Uxg9erxqvA4ZpZdQEzMNa" name="IQUNIX_MQ80_Mechanical_keyboard_002.JPG" alt="A black IQUNIX MQ80 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/3Uxg9erxqvA4ZpZdQEzMNa-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 type for a living and need a comfortable, low-profile keyboard to do so, the IQUNIX MQ80 is an excellent choice. Featuring Kailh Gold Red linear switches that are highly responsive, the MQ80 sounds wonderful too. It’s built extremely well, thanks to its aluminum case, and web-based VIA makes customization straightforward. Oh, and it’s got 1,000Hz polling for casual gaming.</p><p>Few things in life are perfect, though, and the MQ80 isn’t one of those. Its key combos are annoying and slow to register, and the keyboard’s height isn’t adjustable. That $169 price tag may be hard to stomach for some too.</p><p>But if you don’t mind any of those flaws, and all you care about is a beautiful typing experience, then you can’t go wrong with the MQ80.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I finally tried a vertical mouse and it was the ergonomic upgrade my desk setup was missing ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Just like sitting in an office chair all day isn’t good for your health, using a traditional mouse for hours on end does a number on your body. Those small, repetitive movements and awkward hand positions take a serious toll on your wrist, forearm, and even your shoulder — which is why I switched to a <a href="https://www.tomsguide.com/reference/what-is-a-trackball-mouse">trackball mouse</a> many years ago.</p><p>Not too long after I made the switch, though, a different kind of mouse started blowing up. Although the vertical mouse was first introduced by inventor Jack Lo back in 1994, it wasn’t until the launch of the <a href="https://www.tomsguide.com/us/logitech-mx-vertical-price-availability,news-27829.html">Logitech MX Vertical</a> in 2018 that they actually became mainstream.</p><p>Maybe it was because this new device was part of Logitech's excellent <a href="https://www.tomsguide.com/computing/i-went-all-in-on-logitechs-mx-master-series-for-my-new-desk-setup-and-this-accessory-was-the-real-game-changer">MX Master Series</a>, but once the MX Vertical hit store shelves, vertical mice were here to stay. More recently, the company released a cheaper, more compact version called the <a href="https://www.tomsguide.com/reviews/logitech-lift">Logitech Lift</a>. In the years since, other accessory makers have joined the fray, and these days you can find vertical mice from Acer, Anker, ProtoArc, Ugreen, and of course, Keychron.</p><p>Since I was already testing out its new <a href="https://www.tomsguide.com/computing/keyboards/i-used-keychrons-new-split-mechanical-keyboard-for-a-week-and-its-tether-free-design-is-a-complete-game-changer">Q11 Ultra split keyboard</a>, Keychron asked if I’d be interested in trying out its M5 vertical mouse. While I’ve used a trackball as my daily driver for over a decade now, I figured it was finally time to give a vertical mouse a shot. After using the Keychron M5 for the past month, I honestly wish I had sooner.</p><div class="product"><a data-dimension112="59f83a49-ba40-4f59-b9bb-1ec8ec420bdf" data-action="Deal Block" data-label="M5" data-dimension48="M5" data-dimension25="$59" href="https://www.amazon.com/Keychron-Ergonomic-Bluetooth-Rechargeable-Programmable/dp/B0FRMS2F1P?th=1" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1465px;"><p class="vanilla-image-block" style="padding-top:102.39%;"><img id="MKi48rY4XRGSYs2ymYLyJT" name="M5" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/MKi48rY4XRGSYs2ymYLyJT-1920-80.jpg" mos="" align="middle" fullscreen="" width="1465" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Keychron-Ergonomic-Bluetooth-Rechargeable-Programmable/dp/B0FRMS2F1P?th=1" target="_blank" rel="nofollow" data-dimension112="59f83a49-ba40-4f59-b9bb-1ec8ec420bdf" data-action="Deal Block" data-label="M5" data-dimension48="M5" data-dimension25="$59">M5: was $69 now $59</a></strong><br>This wireless vertical mouse features a blazing fast 8K polling rate and can connect via Bluetooth or using the included 2.4 GHz dongle. In addition to a standard scroll wheel up top, you also have a vertical one on the side as well as two extra buttons that you can customize to your liking online using the Keychron Launcher.<a class="view-deal button" href="https://www.amazon.com/Keychron-Ergonomic-Bluetooth-Rechargeable-Programmable/dp/B0FRMS2F1P?th=1" target="_blank" rel="nofollow" data-dimension112="59f83a49-ba40-4f59-b9bb-1ec8ec420bdf" data-action="Deal Block" data-label="M5" data-dimension48="M5" data-dimension25="$59">View Deal</a></p></div><h2 id="form-follows-forearm">Form follows forearm</h2><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.27%;"><img id="ds7QjYGJ2AaUEQ9rXJKxZi" name="Keychron M5" alt="The Keychron M5 vertical mouse unboxed on a desk" src="https://cdn.mos.cms.futurecdn.net/ds7QjYGJ2AaUEQ9rXJKxZi-1920-80.jpg" mos="" align="middle" fullscreen="" width="3000" height="1688" attribution="" endorsement="" class="inline"></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 the name suggests, a vertical mouse takes the design of a traditional mouse and shifts the buttons and scroll wheel to the side. This allows your hand to rest naturally on its edge in a "handshake" position, rather than being forced flat against your desk. The curves around the base and top of the Keychron M5 conform perfectly to your hand, making it incredibly comfortable to hold even for long stretches at a time.</p><p>In the box, alongside the mouse itself, you get a 2.4 GHz USB receiver, a braided USB-C to USB-C cable, and a USB-C to USB-A adapter just in case you’re using an older machine. Everything was really well packaged, and it was nice to see similar accessories to the ones you get with one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> from Keychron.</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.27%;"><img id="AxCNpxGDr4AMxfy5PzhPLm" name="Keychron M5-3" alt="The front USB-C port on the Keychron M5 vertical mouse" src="https://cdn.mos.cms.futurecdn.net/AxCNpxGDr4AMxfy5PzhPLm-1920-80.jpg" mos="" align="middle" fullscreen="" width="3000" height="1688" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>On the front of the M5, there's a USB-C port used for charging or running the mouse in wired mode. Over on the right side, you'll find the standard left and right-click buttons, separated by a nicely textured scroll wheel that's easy to grip with your index finger. </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.27%;"><img id="9X4DE27AsiZCpbcMFdtm4" name="Keychron M5-2" alt="The side buttons on the Keychron M5 vertical mouse" src="https://cdn.mos.cms.futurecdn.net/9X4DE27AsiZCpbcMFdtm4-1920-80.jpg" mos="" align="middle" fullscreen="" width="3000" height="1688" attribution="" endorsement="" class="inline"></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 left side is where things get interesting. In addition to where your thumb rests, Keychron placed a second, wider scroll wheel up near the top. While you can map it to scroll vertically, I used the browser-based Keychron Launcher to set it to horizontal scrolling. This way, I can easily scroll through extra wide spreadsheets horizontally without having to use the scroll bar at the bottom in Google Sheets. </p><p>In my rush to get the M5 unboxed and on my desk, I completely forgot to take a picture of the bottom. On the underside, there’s a mode toggle switch surrounded by three small buttons. One button controls your DPI, another lets you crank the polling rate all the way up to an ultra-responsive 8K and the last one lets you swap between several Bluetooth channels as you can pair this mouse with up to three devices over Bluetooth.</p><p>With the M5 all charged up, it was time to plug into my computer and take it for a scroll.</p><h2 id="sculpted-to-your-workflow">Sculpted to your workflow</h2><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="iqEh2dGs8k8jrCR25BTRnC" name="Keychron Q11 Ultra-3" alt="A desk setup built around the Keychron Q11 Ultra and the Geekom A9 Max 2026 Edition" src="https://cdn.mos.cms.futurecdn.net/iqEh2dGs8k8jrCR25BTRnC-1920-80.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>To test out the Keychron M5, I added it to a productivity-focused desk setup I put together while reviewing the <a href="https://www.tomsguide.com/computing/mini-pcs/geekom-a9-max-review">Geekom A9 Max</a> mini PC. It’s sitting right next to the Q11 Ultra split mechanical keyboard, and I’m using the <a href="https://www.tomsguide.com/computing/monitors/i-ditched-my-dual-monitor-setup-for-a-single-40-inch-ultrawide-and-two-screens-arent-better-than-one">Innocn 40C1U</a> 40-inch ultrawide as my display.</p><p>Even though I recently took a hiatus from my trusty trackball mouse to try out the <a href="https://www.tomsguide.com/computing/mice/logitech-mx-master-4-review">Logitech MX Master 4</a>, it still took me a bit to get fully accustomed to the M5. My hand wanted to rest flat out of habit, but by the second day of using this vertical mouse, that craving went away and having my hand tilted slightly felt like second nature. Once the initial novelty wore off, it was time to put the M5 to work — but before doing so, I wanted to customize those side buttons.</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:3328px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SA35o9F3CFVfyhCYJhdig5" name="keychron-m5-keychron-launcher" alt="A screenshot showing how you can configure the Keychron M5 vertical mouse using the Keychron Launcher in your browser" src="https://cdn.mos.cms.futurecdn.net/SA35o9F3CFVfyhCYJhdig5-1920-80.jpg" mos="" align="middle" fullscreen="" width="3328" height="1872" attribution="" endorsement="" class="inline"></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 that end, I booted up the <a href="https://launcher.keychron.com/#/not-connect" target="_blank">Keychron Launcher</a>, hit the connect button on the top right, and the web app instantly recognized the M5. You can technically customize all of the mouse’s wheels and buttons, but for my purposes, I stuck to tweaking the functionality of the ones on the left side.</p><p>Since I’m constantly working with long blocks of text when uploading and editing articles, I set the button toward the front of the mouse to copy and the one behind it to paste. Then, like I mentioned before, I set the larger scroll wheel above them to scroll horizontally. This took two steps, as I had to manually set scrolling down to go right and scrolling up to go left. Still, I appreciate that you can fine-tune how this extra scroll wheel works to your liking.</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="BxqFVueYrkmQChJfNTKpqf" name="Keychron M5-4" alt="A person using the Keychron M5 vertical mouse" src="https://cdn.mos.cms.futurecdn.net/BxqFVueYrkmQChJfNTKpqf-1920-80.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: Tom's Guide)</span></figcaption></figure><p>In day-to-day use, what surprised me most about the M5 is that I didn’t feel any wrist strain at all, even though this vertical mouse works more like a traditional one with an optical sensor on the bottom as opposed to a trackball controlled entirely by your thumb or index finger. I chalk this up to the fact that Keychron didn’t just make a vertical mouse; they made an exceptionally fast one with an 8K polling rate. I also have my mouse cursor set to go insanely fast across the screen given the 5K2K resolution of the Innocn monitor I’m using.</p><p>As for the M5’s battery life, Keychron claims it can last up to 140 hours on a single charge. I’d say that’s fairly accurate, as during the month or so I’ve spent using this mouse (without turning it off overnight), I’ve only had to charge it once. Keep in mind, though, with the polling rate set above 1,000Hz, the mouse is going to use more power and the battery won’t last nearly as long. However, you could easily turn it up when doing a task where you need that high polling rate and then click it back down afterward using the button on the underside.</p><p>One thing I haven’t tried out with the M5 yet is gaming, but I expect it would work pretty well for that, too. After <a href="https://www.tomsguide.com/gaming/gaming-peripherals/i-just-tested-this-trackball-mouse-for-gaming-and-now-i-might-put-down-my-controller-for-good">using a trackball mouse for gaming</a>, I don’t think it would be that difficult to use a vertical mouse to do the same.</p><h2 id="a-perfect-ergonomic-pair">A perfect ergonomic pair</h2><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="JHqG6SZYPiFZbaWhdoD9VG" name="Keychron Q11 Ultra-2" alt="A person typing on the Keychron Q11 Ultra split mechanical keyboard at their desk" src="https://cdn.mos.cms.futurecdn.net/JHqG6SZYPiFZbaWhdoD9VG-1920-80.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>As a trackball convert turned diehard, I initially wrote off vertical mice. Why would I want to go back to moving my hand across my desk when I could just move my thumb? In fact, when a coworker brought them up as a great ergonomic option, I scoffed at the idea since trackballs already existed. Now, though, I’ve been converted yet again — using a vertical mouse is a lot more comfortable and enjoyable than I ever thought it would be.</p><p>There’s a steep learning curve when switching from a traditional mouse to a trackball. With a vertical mouse, there really isn’t one at all. Sure, your hand might feel slightly out of place for the first hour or so, but by the end of the day, you’ll be clicking and scrolling just like you always have.</p><p>One thing that really helped ease my transition was using it alongside a split keyboard. I had used a split keyboard for several years in the past but eventually gave it up in favor of a standard wireless mechanical keyboard. With my arms resting more naturally with the Q11 separated into halves, it just felt completely right reaching for the M5 when it came time to move my cursor.</p><p>Since I review tech for a living, I switch between different keyboards and mice as often as I try out new mini PCs. However, this combination of the Keychron Q11 keyboard and M5 mouse has really grown on me, and I think I might stick with it for a bit longer than I normally do. If, like me, you’re on the fence about giving a vertical mouse a shot, I highly recommend this one — especially since you can pick it up for around $70.</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/computing/tablets/i-ditched-my-ipad-and-kindle-for-this-tablet-with-a-paper-like-display-and-its-up-to-39-percent-off-for-prime-day">I ditched my iPad and Kindle for this tablet with a paper-like display — and it's up to 39% off for Prime Day</a></li><li><a href="https://www.tomsguide.com/computing/i-screenmaxxed-my-desk-setup-by-going-from-three-to-five-displays-but-they-arent-all-monitors">I screenmaxxed my desk setup by going from three to five displays — but they aren’t all monitors</a></li><li><a href="https://www.tomsguide.com/computing/laptops/i-held-the-macbook-neo-in-one-hand-and-the-new-xps-13-in-the-other-and-dells-affordable-laptop-is-surprisingly-lighter">I held the MacBook Neo in one hand and the new XPS 13 in the other — and Dell’s $699 laptop is surprisingly lighter</a></li></ul><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=vertical+mice&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, .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/computing/peripherals/i-thought-a-vertical-mouse-was-no-match-for-my-trackball-until-i-tried-one-and-now-im-never-going-back</link>
                                                                            <description>
                            <![CDATA[ Using a standard mouse day in and day out takes its toll on your wrist — so I finally decided to give a vertical mouse a try. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">TfBsTbS6DmX6o9HmQ5uUc6</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/y7bcFsms72VaRaXkfgVYW8-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 26 Jun 2026 10:30:00 +0000</pubDate>                                                                                                                                <updated>Mon, 29 Jun 2026 05:31:35 +0000</updated>
                                                                                                                                            <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                <author><![CDATA[ anthony.spadafora@futurenet.com (Anthony Spadafora) ]]></author>                    <dc:creator><![CDATA[ Anthony Spadafora ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/z73LEoj7FkUjNG85GcWHtH-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Anthony Spadafora is the managing editor for security and home office furniture at Tom’s Guide where he covers everything from data breaches and malware to password managers and the best way to cover your whole home or business with a strong Wi-Fi signal.&lt;/p&gt;&lt;p&gt;Before joining the team, he spent three years covering cybersecurity and B2B tech for ITProPortal while living in South Korea. After moving back to the US. Anthony joined the TechRadar Pro team where he covered these topics along with VPNs, web hosting, online collaboration software and video conferencing for four years. Anthony also has his ears to the ground and is on the lookout for the next major cyberattack or data breach.&lt;/p&gt;&lt;p&gt;Based in Houston, Texas, Anthony also handles VPN testing for both Tom’s Guide and TechRadar. As someone who has worked from home exclusively since 2018, he has reviewed dozens of standing desks as well as office chairs and has taken a closer look at other essential remote working accessories. As part of these reviews, Anthony frequently builds intricate desk setups which is why he’s such a big advocate for cable management and keeping things organized. When he’s not writing, he can be found tinkering with PCs and game consoles, managing cables and making upgrades to his smart home.&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/y7bcFsms72VaRaXkfgVYW8-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A person using the Keychron M5 vertical mouse on a desk]]></media:description>                                                            <media:text><![CDATA[A person using the Keychron M5 vertical mouse on a desk]]></media:text>
                                <media:title type="plain"><![CDATA[A person using the Keychron M5 vertical mouse on a desk]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/y7bcFsms72VaRaXkfgVYW8-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Just like sitting in an office chair all day isn’t good for your health, using a traditional mouse for hours on end does a number on your body. Those small, repetitive movements and awkward hand positions take a serious toll on your wrist, forearm, and even your shoulder — which is why I switched to a <a href="https://www.tomsguide.com/reference/what-is-a-trackball-mouse">trackball mouse</a> many years ago.</p><p>Not too long after I made the switch, though, a different kind of mouse started blowing up. Although the vertical mouse was first introduced by inventor Jack Lo back in 1994, it wasn’t until the launch of the <a href="https://www.tomsguide.com/us/logitech-mx-vertical-price-availability,news-27829.html">Logitech MX Vertical</a> in 2018 that they actually became mainstream.</p><p>Maybe it was because this new device was part of Logitech's excellent <a href="https://www.tomsguide.com/computing/i-went-all-in-on-logitechs-mx-master-series-for-my-new-desk-setup-and-this-accessory-was-the-real-game-changer">MX Master Series</a>, but once the MX Vertical hit store shelves, vertical mice were here to stay. More recently, the company released a cheaper, more compact version called the <a href="https://www.tomsguide.com/reviews/logitech-lift">Logitech Lift</a>. In the years since, other accessory makers have joined the fray, and these days you can find vertical mice from Acer, Anker, ProtoArc, Ugreen, and of course, Keychron.</p><p>Since I was already testing out its new <a href="https://www.tomsguide.com/computing/keyboards/i-used-keychrons-new-split-mechanical-keyboard-for-a-week-and-its-tether-free-design-is-a-complete-game-changer">Q11 Ultra split keyboard</a>, Keychron asked if I’d be interested in trying out its M5 vertical mouse. While I’ve used a trackball as my daily driver for over a decade now, I figured it was finally time to give a vertical mouse a shot. After using the Keychron M5 for the past month, I honestly wish I had sooner.</p><div class="product"><a data-dimension112="59f83a49-ba40-4f59-b9bb-1ec8ec420bdf" data-action="Deal Block" data-label="M5" data-dimension48="M5" data-dimension25="$59" href="https://www.amazon.com/Keychron-Ergonomic-Bluetooth-Rechargeable-Programmable/dp/B0FRMS2F1P?th=1" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1465px;"><p class="vanilla-image-block" style="padding-top:102.39%;"><img id="MKi48rY4XRGSYs2ymYLyJT" name="M5" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/MKi48rY4XRGSYs2ymYLyJT-1920-80.jpg" mos="" align="middle" fullscreen="" width="1465" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Keychron-Ergonomic-Bluetooth-Rechargeable-Programmable/dp/B0FRMS2F1P?th=1" target="_blank" rel="nofollow" data-dimension112="59f83a49-ba40-4f59-b9bb-1ec8ec420bdf" data-action="Deal Block" data-label="M5" data-dimension48="M5" data-dimension25="$59">M5: was $69 now $59</a></strong><br>This wireless vertical mouse features a blazing fast 8K polling rate and can connect via Bluetooth or using the included 2.4 GHz dongle. In addition to a standard scroll wheel up top, you also have a vertical one on the side as well as two extra buttons that you can customize to your liking online using the Keychron Launcher.<a class="view-deal button" href="https://www.amazon.com/Keychron-Ergonomic-Bluetooth-Rechargeable-Programmable/dp/B0FRMS2F1P?th=1" target="_blank" rel="nofollow" data-dimension112="59f83a49-ba40-4f59-b9bb-1ec8ec420bdf" data-action="Deal Block" data-label="M5" data-dimension48="M5" data-dimension25="$59">View Deal</a></p></div><h2 id="form-follows-forearm">Form follows forearm</h2><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.27%;"><img id="ds7QjYGJ2AaUEQ9rXJKxZi" name="Keychron M5" alt="The Keychron M5 vertical mouse unboxed on a desk" src="https://cdn.mos.cms.futurecdn.net/ds7QjYGJ2AaUEQ9rXJKxZi-1920-80.jpg" mos="" align="middle" fullscreen="" width="3000" height="1688" attribution="" endorsement="" class="inline"></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 the name suggests, a vertical mouse takes the design of a traditional mouse and shifts the buttons and scroll wheel to the side. This allows your hand to rest naturally on its edge in a "handshake" position, rather than being forced flat against your desk. The curves around the base and top of the Keychron M5 conform perfectly to your hand, making it incredibly comfortable to hold even for long stretches at a time.</p><p>In the box, alongside the mouse itself, you get a 2.4 GHz USB receiver, a braided USB-C to USB-C cable, and a USB-C to USB-A adapter just in case you’re using an older machine. Everything was really well packaged, and it was nice to see similar accessories to the ones you get with one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> from Keychron.</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.27%;"><img id="AxCNpxGDr4AMxfy5PzhPLm" name="Keychron M5-3" alt="The front USB-C port on the Keychron M5 vertical mouse" src="https://cdn.mos.cms.futurecdn.net/AxCNpxGDr4AMxfy5PzhPLm-1920-80.jpg" mos="" align="middle" fullscreen="" width="3000" height="1688" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>On the front of the M5, there's a USB-C port used for charging or running the mouse in wired mode. Over on the right side, you'll find the standard left and right-click buttons, separated by a nicely textured scroll wheel that's easy to grip with your index finger. </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.27%;"><img id="9X4DE27AsiZCpbcMFdtm4" name="Keychron M5-2" alt="The side buttons on the Keychron M5 vertical mouse" src="https://cdn.mos.cms.futurecdn.net/9X4DE27AsiZCpbcMFdtm4-1920-80.jpg" mos="" align="middle" fullscreen="" width="3000" height="1688" attribution="" endorsement="" class="inline"></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 left side is where things get interesting. In addition to where your thumb rests, Keychron placed a second, wider scroll wheel up near the top. While you can map it to scroll vertically, I used the browser-based Keychron Launcher to set it to horizontal scrolling. This way, I can easily scroll through extra wide spreadsheets horizontally without having to use the scroll bar at the bottom in Google Sheets. </p><p>In my rush to get the M5 unboxed and on my desk, I completely forgot to take a picture of the bottom. On the underside, there’s a mode toggle switch surrounded by three small buttons. One button controls your DPI, another lets you crank the polling rate all the way up to an ultra-responsive 8K and the last one lets you swap between several Bluetooth channels as you can pair this mouse with up to three devices over Bluetooth.</p><p>With the M5 all charged up, it was time to plug into my computer and take it for a scroll.</p><h2 id="sculpted-to-your-workflow">Sculpted to your workflow</h2><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="iqEh2dGs8k8jrCR25BTRnC" name="Keychron Q11 Ultra-3" alt="A desk setup built around the Keychron Q11 Ultra and the Geekom A9 Max 2026 Edition" src="https://cdn.mos.cms.futurecdn.net/iqEh2dGs8k8jrCR25BTRnC-1920-80.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>To test out the Keychron M5, I added it to a productivity-focused desk setup I put together while reviewing the <a href="https://www.tomsguide.com/computing/mini-pcs/geekom-a9-max-review">Geekom A9 Max</a> mini PC. It’s sitting right next to the Q11 Ultra split mechanical keyboard, and I’m using the <a href="https://www.tomsguide.com/computing/monitors/i-ditched-my-dual-monitor-setup-for-a-single-40-inch-ultrawide-and-two-screens-arent-better-than-one">Innocn 40C1U</a> 40-inch ultrawide as my display.</p><p>Even though I recently took a hiatus from my trusty trackball mouse to try out the <a href="https://www.tomsguide.com/computing/mice/logitech-mx-master-4-review">Logitech MX Master 4</a>, it still took me a bit to get fully accustomed to the M5. My hand wanted to rest flat out of habit, but by the second day of using this vertical mouse, that craving went away and having my hand tilted slightly felt like second nature. Once the initial novelty wore off, it was time to put the M5 to work — but before doing so, I wanted to customize those side buttons.</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:3328px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="SA35o9F3CFVfyhCYJhdig5" name="keychron-m5-keychron-launcher" alt="A screenshot showing how you can configure the Keychron M5 vertical mouse using the Keychron Launcher in your browser" src="https://cdn.mos.cms.futurecdn.net/SA35o9F3CFVfyhCYJhdig5-1920-80.jpg" mos="" align="middle" fullscreen="" width="3328" height="1872" attribution="" endorsement="" class="inline"></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 that end, I booted up the <a href="https://launcher.keychron.com/#/not-connect" target="_blank">Keychron Launcher</a>, hit the connect button on the top right, and the web app instantly recognized the M5. You can technically customize all of the mouse’s wheels and buttons, but for my purposes, I stuck to tweaking the functionality of the ones on the left side.</p><p>Since I’m constantly working with long blocks of text when uploading and editing articles, I set the button toward the front of the mouse to copy and the one behind it to paste. Then, like I mentioned before, I set the larger scroll wheel above them to scroll horizontally. This took two steps, as I had to manually set scrolling down to go right and scrolling up to go left. Still, I appreciate that you can fine-tune how this extra scroll wheel works to your liking.</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="BxqFVueYrkmQChJfNTKpqf" name="Keychron M5-4" alt="A person using the Keychron M5 vertical mouse" src="https://cdn.mos.cms.futurecdn.net/BxqFVueYrkmQChJfNTKpqf-1920-80.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: Tom's Guide)</span></figcaption></figure><p>In day-to-day use, what surprised me most about the M5 is that I didn’t feel any wrist strain at all, even though this vertical mouse works more like a traditional one with an optical sensor on the bottom as opposed to a trackball controlled entirely by your thumb or index finger. I chalk this up to the fact that Keychron didn’t just make a vertical mouse; they made an exceptionally fast one with an 8K polling rate. I also have my mouse cursor set to go insanely fast across the screen given the 5K2K resolution of the Innocn monitor I’m using.</p><p>As for the M5’s battery life, Keychron claims it can last up to 140 hours on a single charge. I’d say that’s fairly accurate, as during the month or so I’ve spent using this mouse (without turning it off overnight), I’ve only had to charge it once. Keep in mind, though, with the polling rate set above 1,000Hz, the mouse is going to use more power and the battery won’t last nearly as long. However, you could easily turn it up when doing a task where you need that high polling rate and then click it back down afterward using the button on the underside.</p><p>One thing I haven’t tried out with the M5 yet is gaming, but I expect it would work pretty well for that, too. After <a href="https://www.tomsguide.com/gaming/gaming-peripherals/i-just-tested-this-trackball-mouse-for-gaming-and-now-i-might-put-down-my-controller-for-good">using a trackball mouse for gaming</a>, I don’t think it would be that difficult to use a vertical mouse to do the same.</p><h2 id="a-perfect-ergonomic-pair">A perfect ergonomic pair</h2><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="JHqG6SZYPiFZbaWhdoD9VG" name="Keychron Q11 Ultra-2" alt="A person typing on the Keychron Q11 Ultra split mechanical keyboard at their desk" src="https://cdn.mos.cms.futurecdn.net/JHqG6SZYPiFZbaWhdoD9VG-1920-80.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>As a trackball convert turned diehard, I initially wrote off vertical mice. Why would I want to go back to moving my hand across my desk when I could just move my thumb? In fact, when a coworker brought them up as a great ergonomic option, I scoffed at the idea since trackballs already existed. Now, though, I’ve been converted yet again — using a vertical mouse is a lot more comfortable and enjoyable than I ever thought it would be.</p><p>There’s a steep learning curve when switching from a traditional mouse to a trackball. With a vertical mouse, there really isn’t one at all. Sure, your hand might feel slightly out of place for the first hour or so, but by the end of the day, you’ll be clicking and scrolling just like you always have.</p><p>One thing that really helped ease my transition was using it alongside a split keyboard. I had used a split keyboard for several years in the past but eventually gave it up in favor of a standard wireless mechanical keyboard. With my arms resting more naturally with the Q11 separated into halves, it just felt completely right reaching for the M5 when it came time to move my cursor.</p><p>Since I review tech for a living, I switch between different keyboards and mice as often as I try out new mini PCs. However, this combination of the Keychron Q11 keyboard and M5 mouse has really grown on me, and I think I might stick with it for a bit longer than I normally do. If, like me, you’re on the fence about giving a vertical mouse a shot, I highly recommend this one — especially since you can pick it up for around $70.</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/computing/tablets/i-ditched-my-ipad-and-kindle-for-this-tablet-with-a-paper-like-display-and-its-up-to-39-percent-off-for-prime-day">I ditched my iPad and Kindle for this tablet with a paper-like display — and it's up to 39% off for Prime Day</a></li><li><a href="https://www.tomsguide.com/computing/i-screenmaxxed-my-desk-setup-by-going-from-three-to-five-displays-but-they-arent-all-monitors">I screenmaxxed my desk setup by going from three to five displays — but they aren’t all monitors</a></li><li><a href="https://www.tomsguide.com/computing/laptops/i-held-the-macbook-neo-in-one-hand-and-the-new-xps-13-in-the-other-and-dells-affordable-laptop-is-surprisingly-lighter">I held the MacBook Neo in one hand and the new XPS 13 in the other — and Dell’s $699 laptop is surprisingly lighter</a></li></ul><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=vertical+mice&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, .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[ Magnetic, mechanical, wired or wireless? I’ve found the 15 best Prime Day keyboard deals, starting from just $39 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Too much choice can be a bad thing. There are thousands of keyboards out there; how does one even decide which is <em>actually </em>good or worth the money? Simple: you take my advice! Over the last two and a half years, I've tested nearly 100 keyboards, including mechanical, magnetic, wired, and wireless ones.</p><p>Not all of them make the cut to be considered the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards,</a> but there have certainly been some stellar decks along the way. With this year's <a href="https://www.tomsguide.com/live/news/prime-day-2026-handpicked-editor-recommended-deals-live">Amazon Prime Day sales</a> now in full swing, I've seen many of these go on sale, so I've decided to help you cut through the noise by handpicking my favorite deals.</p><p>One of the best deals right now is on the <a href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" target="_blank" rel="nofollow">SteelSeries Apex Pro Mini which is a whopping 45% off</a>, so you can grab it for $132 instead of $239. It's a great little mechanical keyboard for gamers, but if you're a hardcore typist (like me), I'd recommend grabbing the <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">Epomaker Glyph for just $111</a>, so you'd be saving 20%. Or if you're on a <em>really </em>tight budget, the <a href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow">Epomaker HE68 Lite is available for as little as $39</a>!</p><p>There's more where that came from, of course. So, what will it be? Mechanical or Hall Effect? For work or gaming? Here are the best Prime Day keyboard deals from Keychron, Epomaker, NuPhy, Razer, and more!</p><h2 id="meet-your-keyboard-deals-expert">Meet your keyboard 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>Keychron V6 Wired: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-swappable-Non-Transparent/dp/B0B2DMKFM5/" target="_blank" rel="nofollow"><strong>was $99 now $64</strong></a><strong></strong></li><li><strong>Keychron C1 Pro 8K Wired: </strong><a href="https://www.amazon.com/gp/aw/d/B0DYJ9MNMQ/" target="_blank" rel="nofollow"><strong>was $54 now $43</strong></a><strong></strong></li><li><strong>Keychron K2 HE Hall Effect: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Double-Rail-Bluetooth-Compatible/dp/B0DCVPGB9N/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>Keychron V1 Max: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR16KS3F/" target="_blank" rel="nofollow"><strong>was $99 now $79</strong></a><strong></strong></li><li><strong>Epomaker Glyph: </strong><a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>Epomaker Ajazz AK820 Pro: </strong><a href="https://www.amazon.com/gp/aw/d/B0CMWKN55N/" target="_blank" rel="nofollow"><strong>was $68 now $55</strong></a><strong></strong></li><li><strong>Epomaker HE68 Lite: </strong><a href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow"><strong>was $49 now $39</strong></a></li><li><strong>NuPhy Halo96 V2: </strong><a href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>NuPhy Air75 V3: </strong><a href="https://www.amazon.com/nuphy-Air75-V3-Mechanical-White-Brown/dp/B0FFG5J55H/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>NuPhy Node75: </strong><a href="https://www.amazon.com/NuPhy-Node75-Mechanical-Swappable-Keyboards/dp/B0G253CGB7/" target="_blank" rel="nofollow"><strong>was $99 now $79</strong></a></li><li><strong>SteelSeries Apex Pro Mini: </strong><a href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" target="_blank" rel="nofollow"><strong>was $239 now $132</strong></a></li><li><strong>Hexgears Immersion A3 TKL: </strong><a href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow"><strong>was $72 now $49</strong></a><strong></strong></li><li><strong>Royal Kludge L75: </strong><a href="https://www.amazon.com/RK-ROYAL-KLUDGE-Mechanical-Touchable/dp/B0DW44SZ83/" target="_blank" rel="nofollow"><strong>was $79 now $60</strong></a><strong></strong></li><li><strong>Royal Kludge R65 Wired: </strong><a href="https://www.amazon.com/RK-ROYAL-KLUDGE-R65-Mechanical/dp/B0CNP1BG1W/" target="_blank" rel="nofollow"><strong>was $59 now $42</strong></a></li><li><strong>Razer BlackWidow V4: </strong><a href="https://www.amazon.com/Razer-BlackWidow-Mechanical-Gaming-Keyboard/dp/B0C6RJ2YNV/" target="_blank" rel="nofollow"><strong>was $169 now $139</strong></a><strong></strong></li></ul><h2 class="article-body__section" id="section-best-prime-day-keychron-deals"><span>Best Prime Day Keychron deals</span></h2><div class="product"><a data-dimension112="e3f2a416-9964-4193-a8e9-e7cf1e8bb15c" data-action="Deal Block" data-label="V6 Wired" data-dimension48="V6 Wired" data-dimension25="$64" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-swappable-Non-Transparent/dp/B0B2DMKFM5/" 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:100.00%;"><img id="HbSHMBKhMAxCzRts49thKd" name="Keychron V6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/HbSHMBKhMAxCzRts49thKd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/keychron-v6-wired-review" data-dimension112="e3f2a416-9964-4193-a8e9-e7cf1e8bb15c" data-action="Deal Block" data-label="V6 Wired" data-dimension48="V6 Wired" data-dimension25="$64">Keychron V6 Wired</a> delivers the usual you can expect from Keychron: attractive looks, easy typing, lovely sound, and fantastic build quality — all for a much lower price than big-name rival brands. It’s fully hot-swappable and available in a variety of guises to suit your needs.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-swappable-Non-Transparent/dp/B0B2DMKFM5/" target="_blank" rel="nofollow" data-dimension112="e3f2a416-9964-4193-a8e9-e7cf1e8bb15c" data-action="Deal Block" data-label="V6 Wired" data-dimension48="V6 Wired" data-dimension25="$64">View Deal</a></p></div><div class="product"><a data-dimension112="a676f986-59e3-4aff-a0bb-b2f2566ed00e" data-action="Deal Block" data-label="C1 Pro 8K" data-dimension48="C1 Pro 8K" data-dimension25="$43" href="https://www.amazon.com/gp/aw/d/B0DYJ9MNMQ/" 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:62.80%;"><img id="kyCf7NZa43T5aDvHeivgRP" name="Keychron C1 Pro 8K" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/kyCf7NZa43T5aDvHeivgRP-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="942" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Keychron C1 Pro 8K may be wired-only but its performance, productivity and comfort more than make up for that. The keyboard is extremely lightweight and easy to carry around, while its ergonomic and comfortable OSA profile keycaps meant I was typing for hours with complete ease. It’s highly customizable, and polls at up to 8,000Hz, making it a great option for serious gaming once you’ve finished work.<a class="view-deal button" href="https://www.amazon.com/gp/aw/d/B0DYJ9MNMQ/" target="_blank" rel="nofollow" data-dimension112="a676f986-59e3-4aff-a0bb-b2f2566ed00e" data-action="Deal Block" data-label="C1 Pro 8K" data-dimension48="C1 Pro 8K" data-dimension25="$43">View Deal</a></p></div><div class="product"><a data-dimension112="53d38f2f-d9a6-418b-af06-c927801f7f3d" data-action="Deal Block" data-label="K2 HE" data-dimension48="K2 HE" data-dimension25="$111" href="https://www.amazon.com/Keychron-Mechanical-Double-Rail-Bluetooth-Compatible/dp/B0DCVPGB9N/" 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:81.00%;"><img id="oh2G22QtgDHvYuqD3AsaeS" name="Keychron K2 HE" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/oh2G22QtgDHvYuqD3AsaeS-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1215" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Keychron has turned its legendary K2 from a custom productivity board into a gaming monster thanks to Gateron double-rail magnetic switches. The <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review" data-dimension112="53d38f2f-d9a6-418b-af06-c927801f7f3d" data-action="Deal Block" data-label="K2 HE" data-dimension48="K2 HE" data-dimension25="$111">Keychron K2 HE</a> has a roster of features commonly seen in top-tier gaming boards, including rapid trigger, last key prioritization and multiple actuation points.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Double-Rail-Bluetooth-Compatible/dp/B0DCVPGB9N/" target="_blank" rel="nofollow" data-dimension112="53d38f2f-d9a6-418b-af06-c927801f7f3d" data-action="Deal Block" data-label="K2 HE" data-dimension48="K2 HE" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="c5ad50db-b270-4697-9397-559a662e3b6f" data-action="Deal Block" data-label="V1 Max" data-dimension48="V1 Max" data-dimension25="$79" href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR16KS3F/" 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:87.00%;"><img id="BfkrdMCh3eF4NK6wsXHD7i" name="Keychron V1 Max deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/BfkrdMCh3eF4NK6wsXHD7i-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1305" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/keychron-v1-review" data-dimension112="c5ad50db-b270-4697-9397-559a662e3b6f" data-action="Deal Block" data-label="V1 Max" data-dimension48="V1 Max" data-dimension25="$79">Keychron V1 Max</a> is the wireless version of one of my favorite mechanical keyboards, the V1. It gives you everything you want from a mechanical keyboard: a solid, durable build, an outstanding typing experience and sound, hot-swapability and easy-to-use companion software. All this for sub-$100 makes the V1 unbeatable for the price, and it’s the ideal keyboard for office use.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR16KS3F/" target="_blank" rel="nofollow" data-dimension112="c5ad50db-b270-4697-9397-559a662e3b6f" data-action="Deal Block" data-label="V1 Max" data-dimension48="V1 Max" data-dimension25="$79">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-epomaker-deals"><span>Best Prime Day Epomaker deals</span></h2><div class="product"><a data-dimension112="23c118b1-e683-4d00-b493-29a030adbb2b" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1415px;"><p class="vanilla-image-block" style="padding-top:59.72%;"><img id="Eg3Dv2pDn9hbKqxeMTCRgW" name="Epo-Glyph-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Eg3Dv2pDn9hbKqxeMTCRgW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1415" height="845" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review" data-dimension112="23c118b1-e683-4d00-b493-29a030adbb2b" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111">Epomaker Glyph</a> is the most fun mechanical keyboard I’ve had the pleasure of testing. Packing responsive, light Wisteria V2 linear switches and boasting a creamy and buttery  sound profile, the Glyph puts the joy back into typing. The keyboard sports a charming typewriter-like design with dual screens and customizable RGB lighting, and build quality is solid too.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow" data-dimension112="23c118b1-e683-4d00-b493-29a030adbb2b" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="69ce1335-3bda-4ca2-a343-4f9e89d14915" data-action="Deal Block" data-label="AK820 Pro" data-dimension48="AK820 Pro" data-dimension25="$55" href="https://www.amazon.com/gp/aw/d/B0CMWKN55N/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1539px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="qGHKF7uchTC9sPRaAB5xN5" name="Epomaker-Ajazz-AK820-Pro" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/qGHKF7uchTC9sPRaAB5xN5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1539" height="1539" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-ajazz-ak820-pro-review" data-dimension112="69ce1335-3bda-4ca2-a343-4f9e89d14915" data-action="Deal Block" data-label="AK820 Pro" data-dimension48="AK820 Pro" data-dimension25="$55">Epomaker Ajazz AK820 Pro</a> leaves no stone unturned. It looks awesome and is built really well, plus it’s hot-swappable. Typing on this keyboard is a delight, and the intuitive companion software lets you easily remap keys and customize RGB lighting. And the price? Just $55 in this year's Prime Day sales, making this keyboard an absolute steal.<a class="view-deal button" href="https://www.amazon.com/gp/aw/d/B0CMWKN55N/" target="_blank" rel="nofollow" data-dimension112="69ce1335-3bda-4ca2-a343-4f9e89d14915" data-action="Deal Block" data-label="AK820 Pro" data-dimension48="AK820 Pro" data-dimension25="$55">View Deal</a></p></div><div class="product"><a data-dimension112="a24021e1-91e5-443d-a671-aded50e4b448" data-action="Deal Block" data-label="HE68 Lite" data-dimension48="HE68 Lite" data-dimension25="$39" href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1423px;"><p class="vanilla-image-block" style="padding-top:43.50%;"><img id="Yizy4xpnTE5VVsyYbHXuSW" name="HE68 Lite deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Yizy4xpnTE5VVsyYbHXuSW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1423" height="619" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow" data-dimension112="a24021e1-91e5-443d-a671-aded50e4b448" data-action="Deal Block" data-label="HE68 Lite" data-dimension48="HE68 Lite" data-dimension25="$39">HE68 Lite: was $49 now $39</a></strong><br>This is the 'Lite' version of the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> which I absolutely loved testing. It packs 8,000Hz polling, per-key and quad-actuation and more. It looks lovely with its minimalist design, and it’s great to type on too. It’s important to note, though, that this keyboard is wired-only and switch choice is limited. Given how cheap it is, though, neither of those feel like dealbreakers.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow" data-dimension112="a24021e1-91e5-443d-a671-aded50e4b448" data-action="Deal Block" data-label="HE68 Lite" data-dimension48="HE68 Lite" data-dimension25="$39">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-nuphy-deals"><span>Best Prime Day NuPhy deals</span></h2><div class="product"><a data-dimension112="6941f790-df7e-49f0-a083-4699df924cb6" data-action="Deal Block" data-label="Halo96 V2" data-dimension48="Halo96 V2" data-dimension25="$111" href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1420px;"><p class="vanilla-image-block" style="padding-top:73.59%;"><img id="XoYdTnafhgFDCyPem3TDC3" name="Halo96 V2 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XoYdTnafhgFDCyPem3TDC3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1420" height="1045" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow" data-dimension112="6941f790-df7e-49f0-a083-4699df924cb6" data-action="Deal Block" data-label="Halo96 V2" data-dimension48="Halo96 V2" data-dimension25="$111">Halo96 V2: was $139 now $111</a></strong><br>As its name suggests, this is the 96% version of the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-halo-75-96-v2-review">NuPhy Halo75 V2</a>. It's gorgeous to look at, listen to and type on. It polls at 1,000Hz in 2.4G mode, so will be fast enough for gaming, too. We have our gripes about it, but all are pretty minor. This is doubtless a worthy successor to the original Halo75.<a class="view-deal button" href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow" data-dimension112="6941f790-df7e-49f0-a083-4699df924cb6" data-action="Deal Block" data-label="Halo96 V2" data-dimension48="Halo96 V2" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="b4889fe4-b3ba-4acc-b2ff-afab0874622b" data-action="Deal Block" data-label="Air75 V3" data-dimension48="Air75 V3" data-dimension25="$111" href="https://www.amazon.com/nuphy-Air75-V3-Mechanical-White-Brown/dp/B0FFG5J55H/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1491px;"><p class="vanilla-image-block" style="padding-top:71.16%;"><img id="ULk8kwyaD7Ws2j8N7PG9eS" name="NuPhy Air75 V3 Deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ULk8kwyaD7Ws2j8N7PG9eS-1920-80.jpg" mos="" align="middle" fullscreen="" width="1491" height="1061" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review" data-dimension112="b4889fe4-b3ba-4acc-b2ff-afab0874622b" data-action="Deal Block" data-label="Air75 V3" data-dimension48="Air75 V3" data-dimension25="$111">NuPhy Air75 V3</a> improves on its predecessor in every way that matters. It’s better looking, better sounding, with better type feel, better battery life and better firmware. It also uses new Gateron low-profile switches, which feature long 3.5mm travel, giving the Air75 V3 the spacious feel of a standard-profile board.<a class="view-deal button" href="https://www.amazon.com/nuphy-Air75-V3-Mechanical-White-Brown/dp/B0FFG5J55H/" target="_blank" rel="nofollow" data-dimension112="b4889fe4-b3ba-4acc-b2ff-afab0874622b" data-action="Deal Block" data-label="Air75 V3" data-dimension48="Air75 V3" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="b7b3fbc5-a166-4344-ae4b-a2b7b4157113" data-action="Deal Block" data-label="Node75" data-dimension48="Node75" data-dimension25="$79" href="https://www.amazon.com/NuPhy-Node75-Mechanical-Swappable-Keyboards/dp/B0G253CGB7/" 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="FJjJoGigb3dkTKGF4S3aDh" name="NuPhy Node75" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/FJjJoGigb3dkTKGF4S3aDh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1080" height="1080" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Not many keyboards get full 5/5 star ratings, but this is one of those rare instances. The <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review" data-dimension112="b7b3fbc5-a166-4344-ae4b-a2b7b4157113" data-action="Deal Block" data-label="Node75" data-dimension48="Node75" data-dimension25="$79">NuPhy Node75</a> is a stellar mechanical keyboard, with no serious flaws. It types and sounds great, looks good and demonstrates the best implementation of touch control I’ve seen from any keyboard (and I’ve tested a lot). It’s also highly customizable and, best of all, reasonably priced.<a class="view-deal button" href="https://www.amazon.com/NuPhy-Node75-Mechanical-Swappable-Keyboards/dp/B0G253CGB7/" target="_blank" rel="nofollow" data-dimension112="b7b3fbc5-a166-4344-ae4b-a2b7b4157113" data-action="Deal Block" data-label="Node75" data-dimension48="Node75" data-dimension25="$79">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="6a76a2ad-d37b-48ad-a93d-fe727aa9f2eb" data-action="Deal Block" data-label="Apex Pro Mini" data-dimension48="Apex Pro Mini" data-dimension25="$132" href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" 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:100.00%;"><img id="7TrJ45iuWjaREehKf9wuSF" name="SteelSeries Apex Pro Mini.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/7TrJ45iuWjaREehKf9wuSF-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/steelseries-apex-pro-mini-keyboard-review" data-dimension112="6a76a2ad-d37b-48ad-a93d-fe727aa9f2eb" data-action="Deal Block" data-label="Apex Pro Mini" data-dimension48="Apex Pro Mini" data-dimension25="$132">SteelSeries Apex Pro Mini</a> offers a high-quality build and a versatile range of per-key customization options, designed specifically for competitive gamers. This is a fantastic keyboard for gaming and is an easy recommendation if you are looking for a competitive edge, particularly thanks to its Rapid Trigger functionality and switchable onboard profiles.<a class="view-deal button" href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" target="_blank" rel="nofollow" data-dimension112="6a76a2ad-d37b-48ad-a93d-fe727aa9f2eb" data-action="Deal Block" data-label="Apex Pro Mini" data-dimension48="Apex Pro Mini" data-dimension25="$132">View Deal</a></p></div><div class="product"><a data-dimension112="40976910-5215-4341-afc9-a6553c7d5aef" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$49" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1497px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="cJXmt94ox3BRqMu7Lk5oyk" name="Hexgears Immersion A3 TKL" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/cJXmt94ox3BRqMu7Lk5oyk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1497" height="1497" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review" data-dimension112="40976910-5215-4341-afc9-a6553c7d5aef" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$49">Hexgears Immersion A3 TKL</a> is a fantastic low-profile keyboard for productivity, thanks to the super silent Kailh linear switches. Its 1,000Hz polling rate is also well-suited to casual gaming, and its sturdy build and customizable RGB and TFT screen make it a joy to use. This is the keyboard I use when I work from home.<a class="view-deal button" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow" data-dimension112="40976910-5215-4341-afc9-a6553c7d5aef" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$49">View Deal</a></p></div><div class="product"><a data-dimension112="c7152923-93a3-4b61-b0a2-589859afadad" data-action="Deal Block" data-label="R65" data-dimension48="R65" data-dimension25="$42" href="https://www.amazon.com/RK-ROYAL-KLUDGE-R65-Mechanical/dp/B0CNP1BG1W/" 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:67.00%;"><img id="h89KGCfYWJm6eYwwziMvwY" name="Royal Kludge R65 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/h89KGCfYWJm6eYwwziMvwY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1005" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Royal Kludge R65 is a fantastic mechanical keyboard to type on and listen to, even though it's wired-only. That shouldn't hold you back, though, as the keyboard packs hot-swappable switches, dynamic RGB lighting, and its 60% layout makes it perfect to travel with.<a class="view-deal button" href="https://www.amazon.com/RK-ROYAL-KLUDGE-R65-Mechanical/dp/B0CNP1BG1W/" target="_blank" rel="nofollow" data-dimension112="c7152923-93a3-4b61-b0a2-589859afadad" data-action="Deal Block" data-label="R65" data-dimension48="R65" data-dimension25="$42">View Deal</a></p></div><div class="product"><a data-dimension112="586a02da-78db-43bb-85b6-2ab815d28a59" data-action="Deal Block" data-label="L75" data-dimension48="L75" data-dimension25="$60" href="https://www.amazon.com/RK-ROYAL-KLUDGE-Mechanical-Touchable/dp/B0DW44SZ83/" 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.73%;"><img id="3783kwfrGuQjxiYFmHVSVR" name="RK-L75-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/3783kwfrGuQjxiYFmHVSVR-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="611" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review" data-dimension112="586a02da-78db-43bb-85b6-2ab815d28a59" data-action="Deal Block" data-label="L75" data-dimension48="L75" data-dimension25="$60">Royal Kludge L75</a> is one of the best mechanical keyboards I've typed on and tested. It’s fitted with responsive pre-lubed switches that are a dream to type on, and the sound profile is delightful too. The dedicated macro keys and long battery life make it a productivity machine, while web-based companion software eases customization.<a class="view-deal button" href="https://www.amazon.com/RK-ROYAL-KLUDGE-Mechanical-Touchable/dp/B0DW44SZ83/" target="_blank" rel="nofollow" data-dimension112="586a02da-78db-43bb-85b6-2ab815d28a59" data-action="Deal Block" data-label="L75" data-dimension48="L75" data-dimension25="$60">View Deal</a></p></div><div class="product"><a data-dimension112="fc8caa80-9362-4a5c-b94a-c03719ea2b27" data-action="Deal Block" data-label="BlackWidow V4" data-dimension48="BlackWidow V4" data-dimension25="$139" href="https://www.amazon.com/Razer-BlackWidow-Mechanical-Gaming-Keyboard/dp/B0C6RJ2YNV/" 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:57.73%;"><img id="zmGCi5TxctLaG69b6NjZDK" name="Razer BlackWidow V4" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/zmGCi5TxctLaG69b6NjZDK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="866" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/reviews/razer-blackwidow-v4-75" data-dimension112="fc8caa80-9362-4a5c-b94a-c03719ea2b27" data-action="Deal Block" data-label="BlackWidow V4" data-dimension48="BlackWidow V4" data-dimension25="$139">Razer BlackWidow V4</a> is an ideal product for keyboard modders and those curious about the hobby. While its hot-swappable design is the main draw, this keyboard is excellent for both gaming and work right out of the box.<a class="view-deal button" href="https://www.amazon.com/Razer-BlackWidow-Mechanical-Gaming-Keyboard/dp/B0C6RJ2YNV/" target="_blank" rel="nofollow" data-dimension112="fc8caa80-9362-4a5c-b94a-c03719ea2b27" data-action="Deal Block" data-label="BlackWidow V4" data-dimension48="BlackWidow V4" data-dimension25="$139">View Deal</a></p></div><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-keyboard-deals"><span>We're tracking all the best Prime Day keyboard deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Keyboards&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/computing/keyboards/amazon-prime-day-2026-keyboard-deals</link>
                                                                            <description>
                            <![CDATA[ This year’s Amazon Prime Day sales have heavily discounted some of my favorite mechanical and magnetic keyboards, and here are my top picks for gaming and work. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">jfL8Lg42BsitGotsZD5rE5</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/eXC2JcfTfMVGbAxY5MwgsY-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 23 Jun 2026 16:41:04 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/eXC2JcfTfMVGbAxY5MwgsY-1920-80.jpg">
                                                            <media:credit><![CDATA[Keychron / Epomaker / NuPhy / Royal Kludge / Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A selection of mechanical and magnetic keyboards]]></media:description>                                                            <media:text><![CDATA[A selection of mechanical and magnetic keyboards]]></media:text>
                                <media:title type="plain"><![CDATA[A selection of mechanical and magnetic keyboards]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/eXC2JcfTfMVGbAxY5MwgsY-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Too much choice can be a bad thing. There are thousands of keyboards out there; how does one even decide which is <em>actually </em>good or worth the money? Simple: you take my advice! Over the last two and a half years, I've tested nearly 100 keyboards, including mechanical, magnetic, wired, and wireless ones.</p><p>Not all of them make the cut to be considered the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards,</a> but there have certainly been some stellar decks along the way. With this year's <a href="https://www.tomsguide.com/live/news/prime-day-2026-handpicked-editor-recommended-deals-live">Amazon Prime Day sales</a> now in full swing, I've seen many of these go on sale, so I've decided to help you cut through the noise by handpicking my favorite deals.</p><p>One of the best deals right now is on the <a href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" target="_blank" rel="nofollow">SteelSeries Apex Pro Mini which is a whopping 45% off</a>, so you can grab it for $132 instead of $239. It's a great little mechanical keyboard for gamers, but if you're a hardcore typist (like me), I'd recommend grabbing the <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">Epomaker Glyph for just $111</a>, so you'd be saving 20%. Or if you're on a <em>really </em>tight budget, the <a href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow">Epomaker HE68 Lite is available for as little as $39</a>!</p><p>There's more where that came from, of course. So, what will it be? Mechanical or Hall Effect? For work or gaming? Here are the best Prime Day keyboard deals from Keychron, Epomaker, NuPhy, Razer, and more!</p><h2 id="meet-your-keyboard-deals-expert">Meet your keyboard 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>Keychron V6 Wired: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-swappable-Non-Transparent/dp/B0B2DMKFM5/" target="_blank" rel="nofollow"><strong>was $99 now $64</strong></a><strong></strong></li><li><strong>Keychron C1 Pro 8K Wired: </strong><a href="https://www.amazon.com/gp/aw/d/B0DYJ9MNMQ/" target="_blank" rel="nofollow"><strong>was $54 now $43</strong></a><strong></strong></li><li><strong>Keychron K2 HE Hall Effect: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Double-Rail-Bluetooth-Compatible/dp/B0DCVPGB9N/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>Keychron V1 Max: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR16KS3F/" target="_blank" rel="nofollow"><strong>was $99 now $79</strong></a><strong></strong></li><li><strong>Epomaker Glyph: </strong><a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>Epomaker Ajazz AK820 Pro: </strong><a href="https://www.amazon.com/gp/aw/d/B0CMWKN55N/" target="_blank" rel="nofollow"><strong>was $68 now $55</strong></a><strong></strong></li><li><strong>Epomaker HE68 Lite: </strong><a href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow"><strong>was $49 now $39</strong></a></li><li><strong>NuPhy Halo96 V2: </strong><a href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>NuPhy Air75 V3: </strong><a href="https://www.amazon.com/nuphy-Air75-V3-Mechanical-White-Brown/dp/B0FFG5J55H/" target="_blank" rel="nofollow"><strong>was $139 now $111</strong></a><strong></strong></li><li><strong>NuPhy Node75: </strong><a href="https://www.amazon.com/NuPhy-Node75-Mechanical-Swappable-Keyboards/dp/B0G253CGB7/" target="_blank" rel="nofollow"><strong>was $99 now $79</strong></a></li><li><strong>SteelSeries Apex Pro Mini: </strong><a href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" target="_blank" rel="nofollow"><strong>was $239 now $132</strong></a></li><li><strong>Hexgears Immersion A3 TKL: </strong><a href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow"><strong>was $72 now $49</strong></a><strong></strong></li><li><strong>Royal Kludge L75: </strong><a href="https://www.amazon.com/RK-ROYAL-KLUDGE-Mechanical-Touchable/dp/B0DW44SZ83/" target="_blank" rel="nofollow"><strong>was $79 now $60</strong></a><strong></strong></li><li><strong>Royal Kludge R65 Wired: </strong><a href="https://www.amazon.com/RK-ROYAL-KLUDGE-R65-Mechanical/dp/B0CNP1BG1W/" target="_blank" rel="nofollow"><strong>was $59 now $42</strong></a></li><li><strong>Razer BlackWidow V4: </strong><a href="https://www.amazon.com/Razer-BlackWidow-Mechanical-Gaming-Keyboard/dp/B0C6RJ2YNV/" target="_blank" rel="nofollow"><strong>was $169 now $139</strong></a><strong></strong></li></ul><h2 class="article-body__section" id="section-best-prime-day-keychron-deals"><span>Best Prime Day Keychron deals</span></h2><div class="product"><a data-dimension112="e3f2a416-9964-4193-a8e9-e7cf1e8bb15c" data-action="Deal Block" data-label="V6 Wired" data-dimension48="V6 Wired" data-dimension25="$64" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-swappable-Non-Transparent/dp/B0B2DMKFM5/" 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:100.00%;"><img id="HbSHMBKhMAxCzRts49thKd" name="Keychron V6.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/HbSHMBKhMAxCzRts49thKd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/keychron-v6-wired-review" data-dimension112="e3f2a416-9964-4193-a8e9-e7cf1e8bb15c" data-action="Deal Block" data-label="V6 Wired" data-dimension48="V6 Wired" data-dimension25="$64">Keychron V6 Wired</a> delivers the usual you can expect from Keychron: attractive looks, easy typing, lovely sound, and fantastic build quality — all for a much lower price than big-name rival brands. It’s fully hot-swappable and available in a variety of guises to suit your needs.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-swappable-Non-Transparent/dp/B0B2DMKFM5/" target="_blank" rel="nofollow" data-dimension112="e3f2a416-9964-4193-a8e9-e7cf1e8bb15c" data-action="Deal Block" data-label="V6 Wired" data-dimension48="V6 Wired" data-dimension25="$64">View Deal</a></p></div><div class="product"><a data-dimension112="a676f986-59e3-4aff-a0bb-b2f2566ed00e" data-action="Deal Block" data-label="C1 Pro 8K" data-dimension48="C1 Pro 8K" data-dimension25="$43" href="https://www.amazon.com/gp/aw/d/B0DYJ9MNMQ/" 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:62.80%;"><img id="kyCf7NZa43T5aDvHeivgRP" name="Keychron C1 Pro 8K" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/kyCf7NZa43T5aDvHeivgRP-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="942" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Keychron C1 Pro 8K may be wired-only but its performance, productivity and comfort more than make up for that. The keyboard is extremely lightweight and easy to carry around, while its ergonomic and comfortable OSA profile keycaps meant I was typing for hours with complete ease. It’s highly customizable, and polls at up to 8,000Hz, making it a great option for serious gaming once you’ve finished work.<a class="view-deal button" href="https://www.amazon.com/gp/aw/d/B0DYJ9MNMQ/" target="_blank" rel="nofollow" data-dimension112="a676f986-59e3-4aff-a0bb-b2f2566ed00e" data-action="Deal Block" data-label="C1 Pro 8K" data-dimension48="C1 Pro 8K" data-dimension25="$43">View Deal</a></p></div><div class="product"><a data-dimension112="53d38f2f-d9a6-418b-af06-c927801f7f3d" data-action="Deal Block" data-label="K2 HE" data-dimension48="K2 HE" data-dimension25="$111" href="https://www.amazon.com/Keychron-Mechanical-Double-Rail-Bluetooth-Compatible/dp/B0DCVPGB9N/" 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:81.00%;"><img id="oh2G22QtgDHvYuqD3AsaeS" name="Keychron K2 HE" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/oh2G22QtgDHvYuqD3AsaeS-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1215" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Keychron has turned its legendary K2 from a custom productivity board into a gaming monster thanks to Gateron double-rail magnetic switches. The <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review" data-dimension112="53d38f2f-d9a6-418b-af06-c927801f7f3d" data-action="Deal Block" data-label="K2 HE" data-dimension48="K2 HE" data-dimension25="$111">Keychron K2 HE</a> has a roster of features commonly seen in top-tier gaming boards, including rapid trigger, last key prioritization and multiple actuation points.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Double-Rail-Bluetooth-Compatible/dp/B0DCVPGB9N/" target="_blank" rel="nofollow" data-dimension112="53d38f2f-d9a6-418b-af06-c927801f7f3d" data-action="Deal Block" data-label="K2 HE" data-dimension48="K2 HE" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="c5ad50db-b270-4697-9397-559a662e3b6f" data-action="Deal Block" data-label="V1 Max" data-dimension48="V1 Max" data-dimension25="$79" href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR16KS3F/" 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:87.00%;"><img id="BfkrdMCh3eF4NK6wsXHD7i" name="Keychron V1 Max deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/BfkrdMCh3eF4NK6wsXHD7i-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1305" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/keychron-v1-review" data-dimension112="c5ad50db-b270-4697-9397-559a662e3b6f" data-action="Deal Block" data-label="V1 Max" data-dimension48="V1 Max" data-dimension25="$79">Keychron V1 Max</a> is the wireless version of one of my favorite mechanical keyboards, the V1. It gives you everything you want from a mechanical keyboard: a solid, durable build, an outstanding typing experience and sound, hot-swapability and easy-to-use companion software. All this for sub-$100 makes the V1 unbeatable for the price, and it’s the ideal keyboard for office use.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR16KS3F/" target="_blank" rel="nofollow" data-dimension112="c5ad50db-b270-4697-9397-559a662e3b6f" data-action="Deal Block" data-label="V1 Max" data-dimension48="V1 Max" data-dimension25="$79">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-epomaker-deals"><span>Best Prime Day Epomaker deals</span></h2><div class="product"><a data-dimension112="23c118b1-e683-4d00-b493-29a030adbb2b" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1415px;"><p class="vanilla-image-block" style="padding-top:59.72%;"><img id="Eg3Dv2pDn9hbKqxeMTCRgW" name="Epo-Glyph-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Eg3Dv2pDn9hbKqxeMTCRgW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1415" height="845" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review" data-dimension112="23c118b1-e683-4d00-b493-29a030adbb2b" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111">Epomaker Glyph</a> is the most fun mechanical keyboard I’ve had the pleasure of testing. Packing responsive, light Wisteria V2 linear switches and boasting a creamy and buttery  sound profile, the Glyph puts the joy back into typing. The keyboard sports a charming typewriter-like design with dual screens and customizable RGB lighting, and build quality is solid too.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow" data-dimension112="23c118b1-e683-4d00-b493-29a030adbb2b" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="69ce1335-3bda-4ca2-a343-4f9e89d14915" data-action="Deal Block" data-label="AK820 Pro" data-dimension48="AK820 Pro" data-dimension25="$55" href="https://www.amazon.com/gp/aw/d/B0CMWKN55N/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1539px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="qGHKF7uchTC9sPRaAB5xN5" name="Epomaker-Ajazz-AK820-Pro" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/qGHKF7uchTC9sPRaAB5xN5-1920-80.jpg" mos="" align="middle" fullscreen="" width="1539" height="1539" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-ajazz-ak820-pro-review" data-dimension112="69ce1335-3bda-4ca2-a343-4f9e89d14915" data-action="Deal Block" data-label="AK820 Pro" data-dimension48="AK820 Pro" data-dimension25="$55">Epomaker Ajazz AK820 Pro</a> leaves no stone unturned. It looks awesome and is built really well, plus it’s hot-swappable. Typing on this keyboard is a delight, and the intuitive companion software lets you easily remap keys and customize RGB lighting. And the price? Just $55 in this year's Prime Day sales, making this keyboard an absolute steal.<a class="view-deal button" href="https://www.amazon.com/gp/aw/d/B0CMWKN55N/" target="_blank" rel="nofollow" data-dimension112="69ce1335-3bda-4ca2-a343-4f9e89d14915" data-action="Deal Block" data-label="AK820 Pro" data-dimension48="AK820 Pro" data-dimension25="$55">View Deal</a></p></div><div class="product"><a data-dimension112="a24021e1-91e5-443d-a671-aded50e4b448" data-action="Deal Block" data-label="HE68 Lite" data-dimension48="HE68 Lite" data-dimension25="$39" href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1423px;"><p class="vanilla-image-block" style="padding-top:43.50%;"><img id="Yizy4xpnTE5VVsyYbHXuSW" name="HE68 Lite deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Yizy4xpnTE5VVsyYbHXuSW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1423" height="619" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow" data-dimension112="a24021e1-91e5-443d-a671-aded50e4b448" data-action="Deal Block" data-label="HE68 Lite" data-dimension48="HE68 Lite" data-dimension25="$39">HE68 Lite: was $49 now $39</a></strong><br>This is the 'Lite' version of the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> which I absolutely loved testing. It packs 8,000Hz polling, per-key and quad-actuation and more. It looks lovely with its minimalist design, and it’s great to type on too. It’s important to note, though, that this keyboard is wired-only and switch choice is limited. Given how cheap it is, though, neither of those feel like dealbreakers.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-HE68-Lite-Adjustable-Sound-Dampened/dp/B0F7R5G19S/" target="_blank" rel="nofollow" data-dimension112="a24021e1-91e5-443d-a671-aded50e4b448" data-action="Deal Block" data-label="HE68 Lite" data-dimension48="HE68 Lite" data-dimension25="$39">View Deal</a></p></div><h2 class="article-body__section" id="section-best-prime-day-nuphy-deals"><span>Best Prime Day NuPhy deals</span></h2><div class="product"><a data-dimension112="6941f790-df7e-49f0-a083-4699df924cb6" data-action="Deal Block" data-label="Halo96 V2" data-dimension48="Halo96 V2" data-dimension25="$111" href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1420px;"><p class="vanilla-image-block" style="padding-top:73.59%;"><img id="XoYdTnafhgFDCyPem3TDC3" name="Halo96 V2 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/XoYdTnafhgFDCyPem3TDC3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1420" height="1045" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow" data-dimension112="6941f790-df7e-49f0-a083-4699df924cb6" data-action="Deal Block" data-label="Halo96 V2" data-dimension48="Halo96 V2" data-dimension25="$111">Halo96 V2: was $139 now $111</a></strong><br>As its name suggests, this is the 96% version of the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-halo-75-96-v2-review">NuPhy Halo75 V2</a>. It's gorgeous to look at, listen to and type on. It polls at 1,000Hz in 2.4G mode, so will be fast enough for gaming, too. We have our gripes about it, but all are pretty minor. This is doubtless a worthy successor to the original Halo75.<a class="view-deal button" href="https://www.amazon.com/nuphy-Structure-Swappable-Mechanical-2-4GHz-Black/dp/B0D866RPXV/" target="_blank" rel="nofollow" data-dimension112="6941f790-df7e-49f0-a083-4699df924cb6" data-action="Deal Block" data-label="Halo96 V2" data-dimension48="Halo96 V2" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="b4889fe4-b3ba-4acc-b2ff-afab0874622b" data-action="Deal Block" data-label="Air75 V3" data-dimension48="Air75 V3" data-dimension25="$111" href="https://www.amazon.com/nuphy-Air75-V3-Mechanical-White-Brown/dp/B0FFG5J55H/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1491px;"><p class="vanilla-image-block" style="padding-top:71.16%;"><img id="ULk8kwyaD7Ws2j8N7PG9eS" name="NuPhy Air75 V3 Deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ULk8kwyaD7Ws2j8N7PG9eS-1920-80.jpg" mos="" align="middle" fullscreen="" width="1491" height="1061" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review" data-dimension112="b4889fe4-b3ba-4acc-b2ff-afab0874622b" data-action="Deal Block" data-label="Air75 V3" data-dimension48="Air75 V3" data-dimension25="$111">NuPhy Air75 V3</a> improves on its predecessor in every way that matters. It’s better looking, better sounding, with better type feel, better battery life and better firmware. It also uses new Gateron low-profile switches, which feature long 3.5mm travel, giving the Air75 V3 the spacious feel of a standard-profile board.<a class="view-deal button" href="https://www.amazon.com/nuphy-Air75-V3-Mechanical-White-Brown/dp/B0FFG5J55H/" target="_blank" rel="nofollow" data-dimension112="b4889fe4-b3ba-4acc-b2ff-afab0874622b" data-action="Deal Block" data-label="Air75 V3" data-dimension48="Air75 V3" data-dimension25="$111">View Deal</a></p></div><div class="product"><a data-dimension112="b7b3fbc5-a166-4344-ae4b-a2b7b4157113" data-action="Deal Block" data-label="Node75" data-dimension48="Node75" data-dimension25="$79" href="https://www.amazon.com/NuPhy-Node75-Mechanical-Swappable-Keyboards/dp/B0G253CGB7/" 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="FJjJoGigb3dkTKGF4S3aDh" name="NuPhy Node75" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/FJjJoGigb3dkTKGF4S3aDh-1920-80.jpg" mos="" align="middle" fullscreen="" width="1080" height="1080" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Not many keyboards get full 5/5 star ratings, but this is one of those rare instances. The <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review" data-dimension112="b7b3fbc5-a166-4344-ae4b-a2b7b4157113" data-action="Deal Block" data-label="Node75" data-dimension48="Node75" data-dimension25="$79">NuPhy Node75</a> is a stellar mechanical keyboard, with no serious flaws. It types and sounds great, looks good and demonstrates the best implementation of touch control I’ve seen from any keyboard (and I’ve tested a lot). It’s also highly customizable and, best of all, reasonably priced.<a class="view-deal button" href="https://www.amazon.com/NuPhy-Node75-Mechanical-Swappable-Keyboards/dp/B0G253CGB7/" target="_blank" rel="nofollow" data-dimension112="b7b3fbc5-a166-4344-ae4b-a2b7b4157113" data-action="Deal Block" data-label="Node75" data-dimension48="Node75" data-dimension25="$79">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="6a76a2ad-d37b-48ad-a93d-fe727aa9f2eb" data-action="Deal Block" data-label="Apex Pro Mini" data-dimension48="Apex Pro Mini" data-dimension25="$132" href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" 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:100.00%;"><img id="7TrJ45iuWjaREehKf9wuSF" name="SteelSeries Apex Pro Mini.jpg" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/7TrJ45iuWjaREehKf9wuSF-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/steelseries-apex-pro-mini-keyboard-review" data-dimension112="6a76a2ad-d37b-48ad-a93d-fe727aa9f2eb" data-action="Deal Block" data-label="Apex Pro Mini" data-dimension48="Apex Pro Mini" data-dimension25="$132">SteelSeries Apex Pro Mini</a> offers a high-quality build and a versatile range of per-key customization options, designed specifically for competitive gamers. This is a fantastic keyboard for gaming and is an easy recommendation if you are looking for a competitive edge, particularly thanks to its Rapid Trigger functionality and switchable onboard profiles.<a class="view-deal button" href="https://www.amazon.com/SteelSeries-Wireless-Mechanical-Gaming-Keyboard/dp/B0B16JFF54/" target="_blank" rel="nofollow" data-dimension112="6a76a2ad-d37b-48ad-a93d-fe727aa9f2eb" data-action="Deal Block" data-label="Apex Pro Mini" data-dimension48="Apex Pro Mini" data-dimension25="$132">View Deal</a></p></div><div class="product"><a data-dimension112="40976910-5215-4341-afc9-a6553c7d5aef" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$49" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1497px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="cJXmt94ox3BRqMu7Lk5oyk" name="Hexgears Immersion A3 TKL" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/cJXmt94ox3BRqMu7Lk5oyk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1497" height="1497" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review" data-dimension112="40976910-5215-4341-afc9-a6553c7d5aef" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$49">Hexgears Immersion A3 TKL</a> is a fantastic low-profile keyboard for productivity, thanks to the super silent Kailh linear switches. Its 1,000Hz polling rate is also well-suited to casual gaming, and its sturdy build and customizable RGB and TFT screen make it a joy to use. This is the keyboard I use when I work from home.<a class="view-deal button" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow" data-dimension112="40976910-5215-4341-afc9-a6553c7d5aef" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$49">View Deal</a></p></div><div class="product"><a data-dimension112="c7152923-93a3-4b61-b0a2-589859afadad" data-action="Deal Block" data-label="R65" data-dimension48="R65" data-dimension25="$42" href="https://www.amazon.com/RK-ROYAL-KLUDGE-R65-Mechanical/dp/B0CNP1BG1W/" 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:67.00%;"><img id="h89KGCfYWJm6eYwwziMvwY" name="Royal Kludge R65 deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/h89KGCfYWJm6eYwwziMvwY-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1005" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Royal Kludge R65 is a fantastic mechanical keyboard to type on and listen to, even though it's wired-only. That shouldn't hold you back, though, as the keyboard packs hot-swappable switches, dynamic RGB lighting, and its 60% layout makes it perfect to travel with.<a class="view-deal button" href="https://www.amazon.com/RK-ROYAL-KLUDGE-R65-Mechanical/dp/B0CNP1BG1W/" target="_blank" rel="nofollow" data-dimension112="c7152923-93a3-4b61-b0a2-589859afadad" data-action="Deal Block" data-label="R65" data-dimension48="R65" data-dimension25="$42">View Deal</a></p></div><div class="product"><a data-dimension112="586a02da-78db-43bb-85b6-2ab815d28a59" data-action="Deal Block" data-label="L75" data-dimension48="L75" data-dimension25="$60" href="https://www.amazon.com/RK-ROYAL-KLUDGE-Mechanical-Touchable/dp/B0DW44SZ83/" 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.73%;"><img id="3783kwfrGuQjxiYFmHVSVR" name="RK-L75-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/3783kwfrGuQjxiYFmHVSVR-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="611" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review" data-dimension112="586a02da-78db-43bb-85b6-2ab815d28a59" data-action="Deal Block" data-label="L75" data-dimension48="L75" data-dimension25="$60">Royal Kludge L75</a> is one of the best mechanical keyboards I've typed on and tested. It’s fitted with responsive pre-lubed switches that are a dream to type on, and the sound profile is delightful too. The dedicated macro keys and long battery life make it a productivity machine, while web-based companion software eases customization.<a class="view-deal button" href="https://www.amazon.com/RK-ROYAL-KLUDGE-Mechanical-Touchable/dp/B0DW44SZ83/" target="_blank" rel="nofollow" data-dimension112="586a02da-78db-43bb-85b6-2ab815d28a59" data-action="Deal Block" data-label="L75" data-dimension48="L75" data-dimension25="$60">View Deal</a></p></div><div class="product"><a data-dimension112="fc8caa80-9362-4a5c-b94a-c03719ea2b27" data-action="Deal Block" data-label="BlackWidow V4" data-dimension48="BlackWidow V4" data-dimension25="$139" href="https://www.amazon.com/Razer-BlackWidow-Mechanical-Gaming-Keyboard/dp/B0C6RJ2YNV/" 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:57.73%;"><img id="zmGCi5TxctLaG69b6NjZDK" name="Razer BlackWidow V4" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/zmGCi5TxctLaG69b6NjZDK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="866" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/reviews/razer-blackwidow-v4-75" data-dimension112="fc8caa80-9362-4a5c-b94a-c03719ea2b27" data-action="Deal Block" data-label="BlackWidow V4" data-dimension48="BlackWidow V4" data-dimension25="$139">Razer BlackWidow V4</a> is an ideal product for keyboard modders and those curious about the hobby. While its hot-swappable design is the main draw, this keyboard is excellent for both gaming and work right out of the box.<a class="view-deal button" href="https://www.amazon.com/Razer-BlackWidow-Mechanical-Gaming-Keyboard/dp/B0C6RJ2YNV/" target="_blank" rel="nofollow" data-dimension112="fc8caa80-9362-4a5c-b94a-c03719ea2b27" data-action="Deal Block" data-label="BlackWidow V4" data-dimension48="BlackWidow V4" data-dimension25="$139">View Deal</a></p></div><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-keyboard-deals"><span>We're tracking all the best Prime Day keyboard deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Keyboards&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[ The Epomaker Glyph, my dream keyboard, just got a 20% price cut, and it’s easily the best mechanical keyboard deal right now ]]></title>
                                                                                                <dc:content><![CDATA[ <p>It isn't often I get to call something my dream keyboard, but that very thing happened a couple weeks ago when I tested the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a>. I know you should never judge a book by its cover but as soon as I saw the Glyph, I knew I'd like it — and when I was reviewing it, I fell in love with it. Money can't buy happiness but it sure can buy you a keyboard that's an utter delight to type on.</p><p>And this week you don't need to pay full price either, as <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">Amazon Prime Day sales have slashed the Glyph's price by 20%</a>, so you can grab it for just $111. When I say this is a deal you can't pass up, I mean it. I review keyboards for a living and <a href="https://www.tomsguide.com/computing/keyboards/ive-tested-nearly-100-keyboards-and-finally-found-my-dream-deck-heres-why-the-epomaker-glyph-is-my-new-favorite">after having tested nearly 100 decks, <em>this </em>is my dream keyboard</a>. If, like me, you type for a living, there's no better keyboard to buy right now.</p><div class="product"><a data-dimension112="38702d41-6ddf-46c2-b8f2-7040d5d3440c" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1415px;"><p class="vanilla-image-block" style="padding-top:59.72%;"><img id="Eg3Dv2pDn9hbKqxeMTCRgW" name="Epo-Glyph-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Eg3Dv2pDn9hbKqxeMTCRgW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1415" height="845" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Epomaker Glyph is the most fun mechanical keyboard I’ve had the pleasure of testing. Packing responsive, light Wisteria V2 linear switches and boasting a creamy and buttery  sound profile, the Glyph puts the joy back into typing. The keyboard sports a charming typewriter-like design with dual screens and customizable RGB lighting, and build quality is solid too.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow" data-dimension112="38702d41-6ddf-46c2-b8f2-7040d5d3440c" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111">View Deal</a></p></div><p>So, what makes the Glyph <em>so good </em>that I can't stop smiling when I type on it? First of all, its design. The Glyph is a typewriter-style keyboard so it blends vintage and modern aesthetics. You get dual screens that show you the date and time, the keyboard's vitals, and you can even display a GIF or static image on it.</p><p>A return lever is located in the top-right corner and it acts as the Enter key when pushed up and Backspace when pulled down. I know it sounds gimmicky but it's actually a lot of fun! And gimmicks aren't always a bad thing. Adding to the Glyph's typewriter-like design are its rounded double-shot PBT keycaps which are soft to touch and lovely to feel.</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-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 never suggest spending any amount of money on a keyboard that isn't great to type on — and the Glyph offers an outstanding typing experience. Its pre-lubed Epomaker Wisteria V2 linear switches require just 38g of force to activate, and have a pre-travel distance of 2mm (total 3.6mm), so you don't need to press them all the way down to activate them, which means that you can type quickly.</p><p>And type quickly I do. Straight out of the box, I achieved a type speed of 109 words per minute with 93.2% accuracy. Yes, I am a speed demon. It's also hot-swappable which means that you can use any 3- or 5-pin south-facing MX switches with it. Simply pop them in — no need to de- and re-solder them to the PCB.</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="CfTyBr5QuLHymePzaZYCGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CfTyBr5QuLHymePzaZYCGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 could wax lyrical about the Glyph and how much I love it all day long, but I wouldn't want to bore you. One last thing before I stop, though: the Glyph comes with a CloudGel wrist rest that's made of high-quality silicone and PVC materials. It's ridiculously soft to rest your wrists on, and it makes typing all day long very comfortable. Just an added bonus of getting this keyboard over all others.</p><p>Amazon Prime Day sales are a great time to save on your favorite tech, so if you're on the hunt for a new keyboard, this is the one for you. Right now, <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">the Epomaker Glyph is down to just $111</a>, making it an unmissable deal. It's one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> you can buy, especially if you love one that looks a bit outlandish!</p><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-keyboard-deals"><span>We're tracking all the best Prime Day keyboard deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Keyboards&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/computing/keyboards/epomaker-glyph-prime-day-2026-deal</link>
                                                                            <description>
                            <![CDATA[ The Epomaker Glyph is one of the best mechanical keyboards I've ever tested and right now, it's 20% off for Prime Day. An unmissable deal! ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">gnmQzsC74FDpfBJ7h4eVYQ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/muq9iN8gLuFeLiWFE7uwNH-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 23 Jun 2026 13:35:21 +0000</pubDate>                                                                                                                                <updated>Tue, 23 Jun 2026 13:35:30 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/muq9iN8gLuFeLiWFE7uwNH-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[An Epomaker Glyph typewriter-style mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[An Epomaker Glyph typewriter-style mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[An Epomaker Glyph typewriter-style mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/muq9iN8gLuFeLiWFE7uwNH-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>It isn't often I get to call something my dream keyboard, but that very thing happened a couple weeks ago when I tested the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a>. I know you should never judge a book by its cover but as soon as I saw the Glyph, I knew I'd like it — and when I was reviewing it, I fell in love with it. Money can't buy happiness but it sure can buy you a keyboard that's an utter delight to type on.</p><p>And this week you don't need to pay full price either, as <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">Amazon Prime Day sales have slashed the Glyph's price by 20%</a>, so you can grab it for just $111. When I say this is a deal you can't pass up, I mean it. I review keyboards for a living and <a href="https://www.tomsguide.com/computing/keyboards/ive-tested-nearly-100-keyboards-and-finally-found-my-dream-deck-heres-why-the-epomaker-glyph-is-my-new-favorite">after having tested nearly 100 decks, <em>this </em>is my dream keyboard</a>. If, like me, you type for a living, there's no better keyboard to buy right now.</p><div class="product"><a data-dimension112="38702d41-6ddf-46c2-b8f2-7040d5d3440c" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1415px;"><p class="vanilla-image-block" style="padding-top:59.72%;"><img id="Eg3Dv2pDn9hbKqxeMTCRgW" name="Epo-Glyph-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Eg3Dv2pDn9hbKqxeMTCRgW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1415" height="845" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Epomaker Glyph is the most fun mechanical keyboard I’ve had the pleasure of testing. Packing responsive, light Wisteria V2 linear switches and boasting a creamy and buttery  sound profile, the Glyph puts the joy back into typing. The keyboard sports a charming typewriter-like design with dual screens and customizable RGB lighting, and build quality is solid too.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow" data-dimension112="38702d41-6ddf-46c2-b8f2-7040d5d3440c" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$111">View Deal</a></p></div><p>So, what makes the Glyph <em>so good </em>that I can't stop smiling when I type on it? First of all, its design. The Glyph is a typewriter-style keyboard so it blends vintage and modern aesthetics. You get dual screens that show you the date and time, the keyboard's vitals, and you can even display a GIF or static image on it.</p><p>A return lever is located in the top-right corner and it acts as the Enter key when pushed up and Backspace when pulled down. I know it sounds gimmicky but it's actually a lot of fun! And gimmicks aren't always a bad thing. Adding to the Glyph's typewriter-like design are its rounded double-shot PBT keycaps which are soft to touch and lovely to feel.</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-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 never suggest spending any amount of money on a keyboard that isn't great to type on — and the Glyph offers an outstanding typing experience. Its pre-lubed Epomaker Wisteria V2 linear switches require just 38g of force to activate, and have a pre-travel distance of 2mm (total 3.6mm), so you don't need to press them all the way down to activate them, which means that you can type quickly.</p><p>And type quickly I do. Straight out of the box, I achieved a type speed of 109 words per minute with 93.2% accuracy. Yes, I am a speed demon. It's also hot-swappable which means that you can use any 3- or 5-pin south-facing MX switches with it. Simply pop them in — no need to de- and re-solder them to the PCB.</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="CfTyBr5QuLHymePzaZYCGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CfTyBr5QuLHymePzaZYCGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 could wax lyrical about the Glyph and how much I love it all day long, but I wouldn't want to bore you. One last thing before I stop, though: the Glyph comes with a CloudGel wrist rest that's made of high-quality silicone and PVC materials. It's ridiculously soft to rest your wrists on, and it makes typing all day long very comfortable. Just an added bonus of getting this keyboard over all others.</p><p>Amazon Prime Day sales are a great time to save on your favorite tech, so if you're on the hunt for a new keyboard, this is the one for you. Right now, <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">the Epomaker Glyph is down to just $111</a>, making it an unmissable deal. It's one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> you can buy, especially if you love one that looks a bit outlandish!</p><h3 class="article-body__section" id="section-we-re-tracking-all-the-best-prime-day-keyboard-deals"><span>We're tracking all the best Prime Day keyboard deals</span></h3><div class="vizualizer-embed"><div class="tg-df-widget-host" data-widget-config="?search=Keyboards&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[ Tom’s Guide readers told me they’d never buy a TKL keyboard — so I’ve handpicked my top keyboards with a number pad, starting at just $44 ]]></title>
                                                                                                <dc:content><![CDATA[ <p>A week ago, I wrote about <a href="https://www.tomsguide.com/computing/keyboards/ive-tested-nearly-100-keyboards-and-finally-found-my-dream-deck-heres-why-the-epomaker-glyph-is-my-new-favorite">how much I loved the Epomaker Glyph</a>, a typewriter-style mechanical keyboard that's nothing short of a dream. Tom's Guide readers seemed to enjoy the article, and they complimented the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Glyph</a>'s style, but there was a sticking point for them: the lack of a number pad.</p><p>"Personally, I would never buy it — no numeric keypad is a total deal breaker," and "No keypad = dealbreaker," stuck out to me. Personally, I've never really found much use for a number pad, which is why I prefer 75% TKL keyboards. But I know not everyone feels that way, and that not having a numpad can be an issue for many.</p><p>As someone who has tested nearly 100 keyboards so far, I've reviewed keyboards of all shapes and sizes, including ones with number pads. So, here I am, hoping you'll find this listicle helpful if you're looking for a 100% or nearly full-size keyboard with a number pad. I've handpicked my top picks for your benefit. Whether you're looking for the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> or a Hall Effect one with a numpad, I've got you covered.</p><p>Are there any keyboards you love that I haven't included in the list? Let me know in the comments below!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eERnrW"></div>                            </div>                            <script src="https://kwizly.com/embed/eERnrW.js" async></script><h2 class="article-body__section" id="section-quick-links"><span>Quick links</span></h2><ul><li><strong>Keychron B6 Pro: </strong><a href="https://www.amazon.com/Keychron-B6-Pro-Ultra-Thin-Connection/dp/B0D53P4PBY/" target="_blank" rel="nofollow"><strong>$44 / £44</strong></a><strong></strong></li><li><strong>Epomaker TH99: </strong><a href="https://www.amazon.com/EPOMAKER-TH99-Tri-Mode-Mechanical-Swappable/dp/B0DPMDNY8C/" target="_blank" rel="nofollow"><strong>was $82 now $69 / £82</strong></a><strong></strong></li><li><strong>Keychron V6 8K wired: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Keyboard-Programmable-Hot-swappable/dp/B0G3FRLCXW/" target="_blank" rel="nofollow"><strong>$84 / £89</strong></a><strong></strong></li><li><strong>Keychron K10 V2: </strong><a href="https://www.amazon.com/Keychron-Bluetooth-Wireless-Mechanical-Multitasking/dp/B08MLJMDNN/" target="_blank" rel="nofollow"><strong>$94 / £99</strong></a></li><li><strong>NuPhy Node100: </strong><a href="https://www.amazon.com/NuPhy-Node100-Mechanical-Swappable-Keyboards/dp/B0GFSJ7QLV/" target="_blank" rel="nofollow"><strong>$109</strong></a><strong></strong></li><li><strong>NuPhy Air96 V2: </strong><a href="https://www.amazon.com/nuphy-Wireless-Mechanical-Bluetooth-Connection/dp/B0CRKPL7NN/" target="_blank" rel="nofollow"><strong>was $129 now $116 / £133</strong></a><strong></strong></li><li><strong>Keychron K4 HE: </strong><a href="https://www.amazon.com/Keychron-K4-HE-Wireless-Mechanical/dp/B0DZGY1R6C/" target="_blank" rel="nofollow"><strong>$144 / £144</strong></a><strong></strong></li><li><strong>McHose UT98: </strong><a href="https://www.amazon.com/MCHOSE-UT98-Mechanical-Connection-Swappable/dp/B0FQP36W6K/" target="_blank" rel="nofollow"><strong>$155</strong></a><strong></strong></li><li><strong>Cherry XTRFY MX 10.1 Wireless: </strong><a href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Cordless-Low-Profile/dp/B0FQK3QFWM/" target="_blank" rel="nofollow"><strong>$249 / £174</strong></a></li></ul><h2 class="article-body__section" id="section-my-top-picks-for-keyboards-with-numpads"><span>My top picks for keyboards with numpads</span></h2><div class="product"><a data-dimension112="cb6671d5-4c2e-4d52-92d8-988aa61d02d9" data-action="Deal Block" data-label="B6 Pro" data-dimension48="B6 Pro" data-dimension25="$44" href="https://www.amazon.com/Keychron-B6-Pro-Ultra-Thin-Connection/dp/B0D53P4PBY/" 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:31.08%;"><img id="N8uPrCyBFRnCAhGhWxrhNK" name="keychron B6 Pro" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/N8uPrCyBFRnCAhGhWxrhNK-1920-80.jpg" mos="" align="middle" fullscreen="" width="679" height="211" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>It’s difficult to find things wrong with the <a href="https://www.tomsguide.com/computing/peripherals/keychron-b6-pro-keyboard-review" data-dimension112="cb6671d5-4c2e-4d52-92d8-988aa61d02d9" data-action="Deal Block" data-label="B6 Pro" data-dimension48="B6 Pro" data-dimension25="$44">Keychron B6 Pro</a>. A full-size keyboard with 109 keys and still only 1.37 pounds, this is a super lightweight board, with open-source ZMK firmware, Bluetooth, 2.4GHz, and cabled connection capabilities, and its budget $44 price tag is the cherry on top.<a class="view-deal button" href="https://www.amazon.com/Keychron-B6-Pro-Ultra-Thin-Connection/dp/B0D53P4PBY/" target="_blank" rel="nofollow" data-dimension112="cb6671d5-4c2e-4d52-92d8-988aa61d02d9" data-action="Deal Block" data-label="B6 Pro" data-dimension48="B6 Pro" data-dimension25="$44">View Deal</a></p></div><div class="product"><a data-dimension112="843f59d2-2f0a-440e-ba29-28c4b7653b08" data-action="Deal Block" data-label="TH99" data-dimension48="TH99" data-dimension25="$69" href="https://www.amazon.com/EPOMAKER-TH99-Tri-Mode-Mechanical-Swappable/dp/B0DPMDNY8C/" 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:100.00%;"><img id="M7mdQm48nicyZLuLCTB4oX" name="EPOMAKER TH99" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/M7mdQm48nicyZLuLCTB4oX-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th99-review" data-dimension112="843f59d2-2f0a-440e-ba29-28c4b7653b08" data-action="Deal Block" data-label="TH99" data-dimension48="TH99" data-dimension25="$69">Epomaker TH99</a> is a productivity beast thanks to its fantastic, responsive Creamy Jade linear switches. The keyboard has a lovely sound profile and design, and comes with user-friendly software for customization. Its 96% layout makes it great for the office.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-TH99-Tri-Mode-Mechanical-Swappable/dp/B0DPMDNY8C/" target="_blank" rel="nofollow" data-dimension112="843f59d2-2f0a-440e-ba29-28c4b7653b08" data-action="Deal Block" data-label="TH99" data-dimension48="TH99" data-dimension25="$69">View Deal</a></p></div><div class="product"><a data-dimension112="e7d49635-6089-4ee6-bb70-bf7aa6e90e3a" data-action="Deal Block" data-label="V6 8K" data-dimension48="V6 8K" data-dimension25="$84" href="https://www.amazon.com/Keychron-Mechanical-Keyboard-Programmable-Hot-swappable/dp/B0G3FRLCXW/" 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:81.60%;"><img id="GAPefoMhqNXsFRoQnAyi3Q" name="Keychron-V6-8K-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GAPefoMhqNXsFRoQnAyi3Q-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1224" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Keychron V6 8K is the 2026 refresh of the <a href="https://www.tomsguide.com/computing/peripherals/keychron-v6-wired-review" data-dimension112="e7d49635-6089-4ee6-bb70-bf7aa6e90e3a" data-action="Deal Block" data-label="V6 8K" data-dimension48="V6 8K" data-dimension25="$84">Keychron V6</a>, one of our favorite keyboards. It delivers the usual you can expect from Keychron: attractive looks, easy typing, lovely sound, and fantastic build quality — all for a much lower price than big-name rival brands. It’s fully hot-swappable and available in a variety of guises to suit your needs.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Keyboard-Programmable-Hot-swappable/dp/B0G3FRLCXW/" target="_blank" rel="nofollow" data-dimension112="e7d49635-6089-4ee6-bb70-bf7aa6e90e3a" data-action="Deal Block" data-label="V6 8K" data-dimension48="V6 8K" data-dimension25="$84">View Deal</a></p></div><div class="product"><a data-dimension112="1a28ccda-e784-4a22-8984-70480dc4d0a1" data-action="Deal Block" data-label="K10 V2" data-dimension48="K10 V2" data-dimension25="$94" href="https://www.amazon.com/Keychron-Bluetooth-Wireless-Mechanical-Multitasking/dp/B08MLJMDNN/" 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:75.00%;"><img id="oHN8i9Uuw9fFheT72jzfrB" name="Keychron-K10-V2-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/oHN8i9Uuw9fFheT72jzfrB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1125" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>If you want a mechanical keyboard that’s great for modding purposes and fantastic to type on, you won’t go wrong with the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k10-v2-mechanical-keyboard-review" data-dimension112="1a28ccda-e784-4a22-8984-70480dc4d0a1" data-action="Deal Block" data-label="K10 V2" data-dimension48="K10 V2" data-dimension25="$94">Keychron K10 V2</a>. With responsive stock switches and web-based companion software, this is the budget keyboard to beat. It starts at just $94, offering an inexpensive entry into the world of modding.<a class="view-deal button" href="https://www.amazon.com/Keychron-Bluetooth-Wireless-Mechanical-Multitasking/dp/B08MLJMDNN/" target="_blank" rel="nofollow" data-dimension112="1a28ccda-e784-4a22-8984-70480dc4d0a1" data-action="Deal Block" data-label="K10 V2" data-dimension48="K10 V2" data-dimension25="$94">View Deal</a></p></div><div class="product"><a data-dimension112="078ff5a4-e917-4676-a995-d361eb3b4807" data-action="Deal Block" data-label="Node100" data-dimension48="Node100" data-dimension25="$109" href="https://www.amazon.com/NuPhy-Node100-Mechanical-Swappable-Keyboards/dp/B0GFSJ7QLV/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1488px;"><p class="vanilla-image-block" style="padding-top:69.22%;"><img id="ZocPKYaPV62akSbR6Sspib" name="Nuphy-Node100-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ZocPKYaPV62akSbR6Sspib-1920-80.jpg" mos="" align="middle" fullscreen="" width="1488" height="1030" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The NuPhy Node100 is the full-size variant of the 5-star <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review" data-dimension112="078ff5a4-e917-4676-a995-d361eb3b4807" data-action="Deal Block" data-label="Node100" data-dimension48="Node100" data-dimension25="$109">NuPhy Node75</a>. It types and sounds great, looks good, and demonstrates the best implementation of touch control I’ve seen from any keyboard (and I’ve tested a lot). It’s also highly customizable and, best of all, reasonably priced.<a class="view-deal button" href="https://www.amazon.com/NuPhy-Node100-Mechanical-Swappable-Keyboards/dp/B0GFSJ7QLV/" target="_blank" rel="nofollow" data-dimension112="078ff5a4-e917-4676-a995-d361eb3b4807" data-action="Deal Block" data-label="Node100" data-dimension48="Node100" data-dimension25="$109">View Deal</a></p></div><div class="product"><a data-dimension112="2d4f076d-e039-4371-a36f-dcec75862571" data-action="Deal Block" data-label="Air96 V2" data-dimension48="Air96 V2" data-dimension25="$116" href="https://www.amazon.com/nuphy-Wireless-Mechanical-Bluetooth-Connection/dp/B0CRKPL7NN/" 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:51.87%;"><img id="GbiE3C9k3W5N9MNW7kimq4" name="Nuphy-air96-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GbiE3C9k3W5N9MNW7kimq4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="778" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>We've reviewed the 75% model of the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v2-review" data-dimension112="2d4f076d-e039-4371-a36f-dcec75862571" data-action="Deal Block" data-label="Air96 V2" data-dimension48="Air96 V2" data-dimension25="$116">NuPhy Air96 V2</a>, and you can expect the same great looks, sound, and typing experience. It has high levels of connectivity and an impressively large battery, given its low profile. The build quality is top-notch, too, so you're getting your money's worth here.<a class="view-deal button" href="https://www.amazon.com/nuphy-Wireless-Mechanical-Bluetooth-Connection/dp/B0CRKPL7NN/" target="_blank" rel="nofollow" data-dimension112="2d4f076d-e039-4371-a36f-dcec75862571" data-action="Deal Block" data-label="Air96 V2" data-dimension48="Air96 V2" data-dimension25="$116">View Deal</a></p></div><div class="product"><a data-dimension112="6566525c-1ec6-4432-8f10-1da68f8364f0" data-action="Deal Block" data-label="K4 HE" data-dimension48="K4 HE" data-dimension25="$144" href="https://www.amazon.com/Keychron-K4-HE-Wireless-Mechanical/dp/B0DZGY1R6C/" 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:100.00%;"><img id="zJvF7YG7PwyR49ujj45gii" name="Keychron K4 HE" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/zJvF7YG7PwyR49ujj45gii-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>If you’re on a budget and need a powerful magnetic keyboard, the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k4-he-keyboard-review" data-dimension112="6566525c-1ec6-4432-8f10-1da68f8364f0" data-action="Deal Block" data-label="K4 HE" data-dimension48="K4 HE" data-dimension25="$144">Keychron K4 HE</a> should be right up your alley. Sporting lovely looks and a sturdy build, this is a fantastic board for both gaming and productivity. Per-key and quad-actuation let you adjust each key’s sensitivity, while features like Rapid Trigger give you a competitive edge.<a class="view-deal button" href="https://www.amazon.com/Keychron-K4-HE-Wireless-Mechanical/dp/B0DZGY1R6C/" target="_blank" rel="nofollow" data-dimension112="6566525c-1ec6-4432-8f10-1da68f8364f0" data-action="Deal Block" data-label="K4 HE" data-dimension48="K4 HE" data-dimension25="$144">View Deal</a></p></div><div class="product"><a data-dimension112="6ac330e2-4acb-46b8-97bd-58f8ffcc1a4e" data-action="Deal Block" data-label="UT98" data-dimension48="UT98" data-dimension25="$155" href="https://www.amazon.com/MCHOSE-UT98-Mechanical-Connection-Swappable/dp/B0FQP36W6K/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1414px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="rNj9hqq9FPb7HnjWeSMhyf" name="McHose-UT98-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/rNj9hqq9FPb7HnjWeSMhyf-1920-80.png" mos="" align="middle" fullscreen="" width="1414" height="1414" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Looking for a mechanical keyboard that’s a beast for productivity? The <a href="https://www.tomsguide.com/computing/keyboards/mchose-unbox-therapy-ut98-review" data-dimension112="6ac330e2-4acb-46b8-97bd-58f8ffcc1a4e" data-action="Deal Block" data-label="UT98" data-dimension48="UT98" data-dimension25="$155">McHose x Unbox Therapy UT98</a> is the one for you. Its switches are responsive and quick to register your commands, and combined with the lovely sound profile, the keyboard delivers an outstanding typing experience. It comes in retro colorways, too, and the long battery life is the cherry on top.<a class="view-deal button" href="https://www.amazon.com/MCHOSE-UT98-Mechanical-Connection-Swappable/dp/B0FQP36W6K/" target="_blank" rel="nofollow" data-dimension112="6ac330e2-4acb-46b8-97bd-58f8ffcc1a4e" data-action="Deal Block" data-label="UT98" data-dimension48="UT98" data-dimension25="$155">View Deal</a></p></div><div class="product"><a data-dimension112="8e475599-39e5-4a85-81eb-2df90e6f51bc" data-action="Deal Block" data-label="MX 10.1 Wireless" data-dimension48="MX 10.1 Wireless" data-dimension25="$249" href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Cordless-Low-Profile/dp/B0FQK3QFWM/" 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:24.27%;"><img id="QUhJX7G8nGzyVuoHh3Ra5Z" name="Cherry-MX10.1-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/QUhJX7G8nGzyVuoHh3Ra5Z-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="364" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review" data-dimension112="8e475599-39e5-4a85-81eb-2df90e6f51bc" data-action="Deal Block" data-label="MX 10.1 Wireless" data-dimension48="MX 10.1 Wireless" data-dimension25="$249">Cherry XTRFY MX 10.1 Wireless</a> is a great mechanical keyboard. It’s built well and sports a comfortable low-profile design, which makes it fantastic for all-day typing. Its low-profile switches are responsive in both typing and gaming, and it packs 8,000Hz wired polling too.<a class="view-deal button" href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Cordless-Low-Profile/dp/B0FQK3QFWM/" target="_blank" rel="nofollow" data-dimension112="8e475599-39e5-4a85-81eb-2df90e6f51bc" data-action="Deal Block" data-label="MX 10.1 Wireless" data-dimension48="MX 10.1 Wireless" data-dimension25="$249">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/computing/keyboards/ive-tested-nearly-100-keyboards-and-finally-found-my-dream-deck-heres-why-the-epomaker-glyph-is-my-new-favorite">I've tested nearly 100 keyboards and finally found my dream deck — here’s why the Epomaker Glyph is my new favorite</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li><li><a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">Best mechanical keyboards in 2026</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/my-top-keyboards-with-a-number-pad</link>
                                                                            <description>
                            <![CDATA[ Whether you're looking for a mechanical or Hall Effect keyboard with a number pad, I've got you covered with my top-rated picks, ranging from $44 to $259! ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">f5SdTZGRseRp2539X5RMZF</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/FqJfSLrP2hFxp6kSmi696h-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 20 Jun 2026 05:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/FqJfSLrP2hFxp6kSmi696h-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Keychron K4 HE]]></media:description>                                                            <media:text><![CDATA[A Keychron K4 HE]]></media:text>
                                <media:title type="plain"><![CDATA[A Keychron K4 HE]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/FqJfSLrP2hFxp6kSmi696h-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>A week ago, I wrote about <a href="https://www.tomsguide.com/computing/keyboards/ive-tested-nearly-100-keyboards-and-finally-found-my-dream-deck-heres-why-the-epomaker-glyph-is-my-new-favorite">how much I loved the Epomaker Glyph</a>, a typewriter-style mechanical keyboard that's nothing short of a dream. Tom's Guide readers seemed to enjoy the article, and they complimented the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Glyph</a>'s style, but there was a sticking point for them: the lack of a number pad.</p><p>"Personally, I would never buy it — no numeric keypad is a total deal breaker," and "No keypad = dealbreaker," stuck out to me. Personally, I've never really found much use for a number pad, which is why I prefer 75% TKL keyboards. But I know not everyone feels that way, and that not having a numpad can be an issue for many.</p><p>As someone who has tested nearly 100 keyboards so far, I've reviewed keyboards of all shapes and sizes, including ones with number pads. So, here I am, hoping you'll find this listicle helpful if you're looking for a 100% or nearly full-size keyboard with a number pad. I've handpicked my top picks for your benefit. Whether you're looking for the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> or a Hall Effect one with a numpad, I've got you covered.</p><p>Are there any keyboards you love that I haven't included in the list? Let me know in the comments below!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eERnrW"></div>                            </div>                            <script src="https://kwizly.com/embed/eERnrW.js" async></script><h2 class="article-body__section" id="section-quick-links"><span>Quick links</span></h2><ul><li><strong>Keychron B6 Pro: </strong><a href="https://www.amazon.com/Keychron-B6-Pro-Ultra-Thin-Connection/dp/B0D53P4PBY/" target="_blank" rel="nofollow"><strong>$44 / £44</strong></a><strong></strong></li><li><strong>Epomaker TH99: </strong><a href="https://www.amazon.com/EPOMAKER-TH99-Tri-Mode-Mechanical-Swappable/dp/B0DPMDNY8C/" target="_blank" rel="nofollow"><strong>was $82 now $69 / £82</strong></a><strong></strong></li><li><strong>Keychron V6 8K wired: </strong><a href="https://www.amazon.com/Keychron-Mechanical-Keyboard-Programmable-Hot-swappable/dp/B0G3FRLCXW/" target="_blank" rel="nofollow"><strong>$84 / £89</strong></a><strong></strong></li><li><strong>Keychron K10 V2: </strong><a href="https://www.amazon.com/Keychron-Bluetooth-Wireless-Mechanical-Multitasking/dp/B08MLJMDNN/" target="_blank" rel="nofollow"><strong>$94 / £99</strong></a></li><li><strong>NuPhy Node100: </strong><a href="https://www.amazon.com/NuPhy-Node100-Mechanical-Swappable-Keyboards/dp/B0GFSJ7QLV/" target="_blank" rel="nofollow"><strong>$109</strong></a><strong></strong></li><li><strong>NuPhy Air96 V2: </strong><a href="https://www.amazon.com/nuphy-Wireless-Mechanical-Bluetooth-Connection/dp/B0CRKPL7NN/" target="_blank" rel="nofollow"><strong>was $129 now $116 / £133</strong></a><strong></strong></li><li><strong>Keychron K4 HE: </strong><a href="https://www.amazon.com/Keychron-K4-HE-Wireless-Mechanical/dp/B0DZGY1R6C/" target="_blank" rel="nofollow"><strong>$144 / £144</strong></a><strong></strong></li><li><strong>McHose UT98: </strong><a href="https://www.amazon.com/MCHOSE-UT98-Mechanical-Connection-Swappable/dp/B0FQP36W6K/" target="_blank" rel="nofollow"><strong>$155</strong></a><strong></strong></li><li><strong>Cherry XTRFY MX 10.1 Wireless: </strong><a href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Cordless-Low-Profile/dp/B0FQK3QFWM/" target="_blank" rel="nofollow"><strong>$249 / £174</strong></a></li></ul><h2 class="article-body__section" id="section-my-top-picks-for-keyboards-with-numpads"><span>My top picks for keyboards with numpads</span></h2><div class="product"><a data-dimension112="cb6671d5-4c2e-4d52-92d8-988aa61d02d9" data-action="Deal Block" data-label="B6 Pro" data-dimension48="B6 Pro" data-dimension25="$44" href="https://www.amazon.com/Keychron-B6-Pro-Ultra-Thin-Connection/dp/B0D53P4PBY/" 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:31.08%;"><img id="N8uPrCyBFRnCAhGhWxrhNK" name="keychron B6 Pro" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/N8uPrCyBFRnCAhGhWxrhNK-1920-80.jpg" mos="" align="middle" fullscreen="" width="679" height="211" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>It’s difficult to find things wrong with the <a href="https://www.tomsguide.com/computing/peripherals/keychron-b6-pro-keyboard-review" data-dimension112="cb6671d5-4c2e-4d52-92d8-988aa61d02d9" data-action="Deal Block" data-label="B6 Pro" data-dimension48="B6 Pro" data-dimension25="$44">Keychron B6 Pro</a>. A full-size keyboard with 109 keys and still only 1.37 pounds, this is a super lightweight board, with open-source ZMK firmware, Bluetooth, 2.4GHz, and cabled connection capabilities, and its budget $44 price tag is the cherry on top.<a class="view-deal button" href="https://www.amazon.com/Keychron-B6-Pro-Ultra-Thin-Connection/dp/B0D53P4PBY/" target="_blank" rel="nofollow" data-dimension112="cb6671d5-4c2e-4d52-92d8-988aa61d02d9" data-action="Deal Block" data-label="B6 Pro" data-dimension48="B6 Pro" data-dimension25="$44">View Deal</a></p></div><div class="product"><a data-dimension112="843f59d2-2f0a-440e-ba29-28c4b7653b08" data-action="Deal Block" data-label="TH99" data-dimension48="TH99" data-dimension25="$69" href="https://www.amazon.com/EPOMAKER-TH99-Tri-Mode-Mechanical-Swappable/dp/B0DPMDNY8C/" 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:100.00%;"><img id="M7mdQm48nicyZLuLCTB4oX" name="EPOMAKER TH99" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/M7mdQm48nicyZLuLCTB4oX-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th99-review" data-dimension112="843f59d2-2f0a-440e-ba29-28c4b7653b08" data-action="Deal Block" data-label="TH99" data-dimension48="TH99" data-dimension25="$69">Epomaker TH99</a> is a productivity beast thanks to its fantastic, responsive Creamy Jade linear switches. The keyboard has a lovely sound profile and design, and comes with user-friendly software for customization. Its 96% layout makes it great for the office.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-TH99-Tri-Mode-Mechanical-Swappable/dp/B0DPMDNY8C/" target="_blank" rel="nofollow" data-dimension112="843f59d2-2f0a-440e-ba29-28c4b7653b08" data-action="Deal Block" data-label="TH99" data-dimension48="TH99" data-dimension25="$69">View Deal</a></p></div><div class="product"><a data-dimension112="e7d49635-6089-4ee6-bb70-bf7aa6e90e3a" data-action="Deal Block" data-label="V6 8K" data-dimension48="V6 8K" data-dimension25="$84" href="https://www.amazon.com/Keychron-Mechanical-Keyboard-Programmable-Hot-swappable/dp/B0G3FRLCXW/" 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:81.60%;"><img id="GAPefoMhqNXsFRoQnAyi3Q" name="Keychron-V6-8K-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GAPefoMhqNXsFRoQnAyi3Q-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1224" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Keychron V6 8K is the 2026 refresh of the <a href="https://www.tomsguide.com/computing/peripherals/keychron-v6-wired-review" data-dimension112="e7d49635-6089-4ee6-bb70-bf7aa6e90e3a" data-action="Deal Block" data-label="V6 8K" data-dimension48="V6 8K" data-dimension25="$84">Keychron V6</a>, one of our favorite keyboards. It delivers the usual you can expect from Keychron: attractive looks, easy typing, lovely sound, and fantastic build quality — all for a much lower price than big-name rival brands. It’s fully hot-swappable and available in a variety of guises to suit your needs.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Keyboard-Programmable-Hot-swappable/dp/B0G3FRLCXW/" target="_blank" rel="nofollow" data-dimension112="e7d49635-6089-4ee6-bb70-bf7aa6e90e3a" data-action="Deal Block" data-label="V6 8K" data-dimension48="V6 8K" data-dimension25="$84">View Deal</a></p></div><div class="product"><a data-dimension112="1a28ccda-e784-4a22-8984-70480dc4d0a1" data-action="Deal Block" data-label="K10 V2" data-dimension48="K10 V2" data-dimension25="$94" href="https://www.amazon.com/Keychron-Bluetooth-Wireless-Mechanical-Multitasking/dp/B08MLJMDNN/" 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:75.00%;"><img id="oHN8i9Uuw9fFheT72jzfrB" name="Keychron-K10-V2-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/oHN8i9Uuw9fFheT72jzfrB-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1125" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>If you want a mechanical keyboard that’s great for modding purposes and fantastic to type on, you won’t go wrong with the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k10-v2-mechanical-keyboard-review" data-dimension112="1a28ccda-e784-4a22-8984-70480dc4d0a1" data-action="Deal Block" data-label="K10 V2" data-dimension48="K10 V2" data-dimension25="$94">Keychron K10 V2</a>. With responsive stock switches and web-based companion software, this is the budget keyboard to beat. It starts at just $94, offering an inexpensive entry into the world of modding.<a class="view-deal button" href="https://www.amazon.com/Keychron-Bluetooth-Wireless-Mechanical-Multitasking/dp/B08MLJMDNN/" target="_blank" rel="nofollow" data-dimension112="1a28ccda-e784-4a22-8984-70480dc4d0a1" data-action="Deal Block" data-label="K10 V2" data-dimension48="K10 V2" data-dimension25="$94">View Deal</a></p></div><div class="product"><a data-dimension112="078ff5a4-e917-4676-a995-d361eb3b4807" data-action="Deal Block" data-label="Node100" data-dimension48="Node100" data-dimension25="$109" href="https://www.amazon.com/NuPhy-Node100-Mechanical-Swappable-Keyboards/dp/B0GFSJ7QLV/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1488px;"><p class="vanilla-image-block" style="padding-top:69.22%;"><img id="ZocPKYaPV62akSbR6Sspib" name="Nuphy-Node100-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ZocPKYaPV62akSbR6Sspib-1920-80.jpg" mos="" align="middle" fullscreen="" width="1488" height="1030" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The NuPhy Node100 is the full-size variant of the 5-star <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review" data-dimension112="078ff5a4-e917-4676-a995-d361eb3b4807" data-action="Deal Block" data-label="Node100" data-dimension48="Node100" data-dimension25="$109">NuPhy Node75</a>. It types and sounds great, looks good, and demonstrates the best implementation of touch control I’ve seen from any keyboard (and I’ve tested a lot). It’s also highly customizable and, best of all, reasonably priced.<a class="view-deal button" href="https://www.amazon.com/NuPhy-Node100-Mechanical-Swappable-Keyboards/dp/B0GFSJ7QLV/" target="_blank" rel="nofollow" data-dimension112="078ff5a4-e917-4676-a995-d361eb3b4807" data-action="Deal Block" data-label="Node100" data-dimension48="Node100" data-dimension25="$109">View Deal</a></p></div><div class="product"><a data-dimension112="2d4f076d-e039-4371-a36f-dcec75862571" data-action="Deal Block" data-label="Air96 V2" data-dimension48="Air96 V2" data-dimension25="$116" href="https://www.amazon.com/nuphy-Wireless-Mechanical-Bluetooth-Connection/dp/B0CRKPL7NN/" 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:51.87%;"><img id="GbiE3C9k3W5N9MNW7kimq4" name="Nuphy-air96-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/GbiE3C9k3W5N9MNW7kimq4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="778" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>We've reviewed the 75% model of the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v2-review" data-dimension112="2d4f076d-e039-4371-a36f-dcec75862571" data-action="Deal Block" data-label="Air96 V2" data-dimension48="Air96 V2" data-dimension25="$116">NuPhy Air96 V2</a>, and you can expect the same great looks, sound, and typing experience. It has high levels of connectivity and an impressively large battery, given its low profile. The build quality is top-notch, too, so you're getting your money's worth here.<a class="view-deal button" href="https://www.amazon.com/nuphy-Wireless-Mechanical-Bluetooth-Connection/dp/B0CRKPL7NN/" target="_blank" rel="nofollow" data-dimension112="2d4f076d-e039-4371-a36f-dcec75862571" data-action="Deal Block" data-label="Air96 V2" data-dimension48="Air96 V2" data-dimension25="$116">View Deal</a></p></div><div class="product"><a data-dimension112="6566525c-1ec6-4432-8f10-1da68f8364f0" data-action="Deal Block" data-label="K4 HE" data-dimension48="K4 HE" data-dimension25="$144" href="https://www.amazon.com/Keychron-K4-HE-Wireless-Mechanical/dp/B0DZGY1R6C/" 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:100.00%;"><img id="zJvF7YG7PwyR49ujj45gii" name="Keychron K4 HE" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/zJvF7YG7PwyR49ujj45gii-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1500" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>If you’re on a budget and need a powerful magnetic keyboard, the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k4-he-keyboard-review" data-dimension112="6566525c-1ec6-4432-8f10-1da68f8364f0" data-action="Deal Block" data-label="K4 HE" data-dimension48="K4 HE" data-dimension25="$144">Keychron K4 HE</a> should be right up your alley. Sporting lovely looks and a sturdy build, this is a fantastic board for both gaming and productivity. Per-key and quad-actuation let you adjust each key’s sensitivity, while features like Rapid Trigger give you a competitive edge.<a class="view-deal button" href="https://www.amazon.com/Keychron-K4-HE-Wireless-Mechanical/dp/B0DZGY1R6C/" target="_blank" rel="nofollow" data-dimension112="6566525c-1ec6-4432-8f10-1da68f8364f0" data-action="Deal Block" data-label="K4 HE" data-dimension48="K4 HE" data-dimension25="$144">View Deal</a></p></div><div class="product"><a data-dimension112="6ac330e2-4acb-46b8-97bd-58f8ffcc1a4e" data-action="Deal Block" data-label="UT98" data-dimension48="UT98" data-dimension25="$155" href="https://www.amazon.com/MCHOSE-UT98-Mechanical-Connection-Swappable/dp/B0FQP36W6K/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1414px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="rNj9hqq9FPb7HnjWeSMhyf" name="McHose-UT98-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/rNj9hqq9FPb7HnjWeSMhyf-1920-80.png" mos="" align="middle" fullscreen="" width="1414" height="1414" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Looking for a mechanical keyboard that’s a beast for productivity? The <a href="https://www.tomsguide.com/computing/keyboards/mchose-unbox-therapy-ut98-review" data-dimension112="6ac330e2-4acb-46b8-97bd-58f8ffcc1a4e" data-action="Deal Block" data-label="UT98" data-dimension48="UT98" data-dimension25="$155">McHose x Unbox Therapy UT98</a> is the one for you. Its switches are responsive and quick to register your commands, and combined with the lovely sound profile, the keyboard delivers an outstanding typing experience. It comes in retro colorways, too, and the long battery life is the cherry on top.<a class="view-deal button" href="https://www.amazon.com/MCHOSE-UT98-Mechanical-Connection-Swappable/dp/B0FQP36W6K/" target="_blank" rel="nofollow" data-dimension112="6ac330e2-4acb-46b8-97bd-58f8ffcc1a4e" data-action="Deal Block" data-label="UT98" data-dimension48="UT98" data-dimension25="$155">View Deal</a></p></div><div class="product"><a data-dimension112="8e475599-39e5-4a85-81eb-2df90e6f51bc" data-action="Deal Block" data-label="MX 10.1 Wireless" data-dimension48="MX 10.1 Wireless" data-dimension25="$249" href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Cordless-Low-Profile/dp/B0FQK3QFWM/" 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:24.27%;"><img id="QUhJX7G8nGzyVuoHh3Ra5Z" name="Cherry-MX10.1-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/QUhJX7G8nGzyVuoHh3Ra5Z-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="364" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review" data-dimension112="8e475599-39e5-4a85-81eb-2df90e6f51bc" data-action="Deal Block" data-label="MX 10.1 Wireless" data-dimension48="MX 10.1 Wireless" data-dimension25="$249">Cherry XTRFY MX 10.1 Wireless</a> is a great mechanical keyboard. It’s built well and sports a comfortable low-profile design, which makes it fantastic for all-day typing. Its low-profile switches are responsive in both typing and gaming, and it packs 8,000Hz wired polling too.<a class="view-deal button" href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Cordless-Low-Profile/dp/B0FQK3QFWM/" target="_blank" rel="nofollow" data-dimension112="8e475599-39e5-4a85-81eb-2df90e6f51bc" data-action="Deal Block" data-label="MX 10.1 Wireless" data-dimension48="MX 10.1 Wireless" data-dimension25="$249">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/computing/keyboards/ive-tested-nearly-100-keyboards-and-finally-found-my-dream-deck-heres-why-the-epomaker-glyph-is-my-new-favorite">I've tested nearly 100 keyboards and finally found my dream deck — here’s why the Epomaker Glyph is my new favorite</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li><li><a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">Best mechanical keyboards in 2026</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I've tested nearly 100 keyboards and finally found my dream deck — here’s why the Epomaker Glyph is my new favorite ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I never thought the words "my dream keyboard" would come out of my mouth. Two and a half years ago, I didn't really care much about keyboards — and I was then introduced to the vast world of mechanical and Hall Effect decks. Things haven't been the same ever since. I've come to realize that a good keyboard can really uplift your mood, and make you a better typist — and as someone whose bread and butter depends on typing, I <em>need </em>a powerful deck.</p><p>I've reviewed nearly 100 keyboards during those last two and a half years, and I think my search for the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> is finally over. I've spent the last two weeks typing on the typewriter-style <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a>, and it upstages every other productivity keyboard in more ways than one.</p><div class="product"><a data-dimension112="91c057b5-f0bd-448e-a5a0-903b44b62ba3" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$132" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1415px;"><p class="vanilla-image-block" style="padding-top:59.72%;"><img id="Eg3Dv2pDn9hbKqxeMTCRgW" name="Epo-Glyph-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Eg3Dv2pDn9hbKqxeMTCRgW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1415" height="845" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Epomaker Glyph is the most fun mechanical keyboard I’ve had the pleasure of testing. Packing responsive, light Wisteria V2 linear switches and boasting a creamy and buttery  sound profile, the Glyph puts the joy back into typing. The keyboard sports a charming typewriter-like design with dual screens and customizable RGB lighting, and build quality is solid too.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow" data-dimension112="91c057b5-f0bd-448e-a5a0-903b44b62ba3" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$132">View Deal</a></p></div><p>The Glyph is perhaps one of the best built and most beautifully designed keyboards I've ever tested. Its rounded, typewriter-like keycaps and creamy sound profile are sure to draw eyeballs to your desk — in the best way possible. Dual screens that show the board's vitals (or GIFs) are a nice touch, and honestly, there's nothing I'd change about the Glyph. Here's why it's my dream keyboard.</p><h2 id="drool-worthy-design">Drool-worthy 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="BaY3N4p2Sup3v7bcrdva5g" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/BaY3N4p2Sup3v7bcrdva5g-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 so much to love about the Epomaker Glyph, and its typewriter-like design is one of its most striking features. The Glyph's body is covered in Retro White while its rounded keycaps utilize a combination of sage green and off-white. It's soft and glossy to look at and touch, and when you throw in south-facing RGB lighting as well as an ambient light strip that falls on the surface the keyboard is sitting on, you've got yourself a winner.</p><p>The Glyph isn't a "typewriter-style" keyboard only by name, and it's got a specific typewriter-like function to prove it. A return lever is located in the top-left corner which acts as the Enter key when pushed up and Backspace when pulled down. I know that you're thinking that's so gimmicky, and I thought the same when I first saw the keyboard — but I've been using it every so often while typing, and it's great fun.</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:400px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hMd6veZFaS8PiCtJt6wic6" name="Epomaker-Glyph" alt="A GIF of puffings displayed on an Epomaker Glyph keyboard" src="https://cdn.mos.cms.futurecdn.net/hMd6veZFaS8PiCtJt6wic6-1920-80.gif" mos="" align="middle" fullscreen="" width="400" height="225" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: National Geographic / Tom's Guide)</span></figcaption></figure><p>What I love about the Glyph is its dual screens which help it strike a balance between modern functionality and retro aesthetics. These screens display the date and time, and the keyboard's vitals, such as battery life, connection mode, active operating system, CPU usage, and more. Or you could display a GIF of puffins, like I did!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-XmAwEX"></div>                            </div>                            <script src="https://kwizly.com/embed/XmAwEX.js" async></script><h2 id="call-me-a-speed-demon">Call me a speed demon</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="74p25fJhGbVJscmLFXa28f" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/74p25fJhGbVJscmLFXa28f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>A keyboard's looks mean nothing if it isn't good to type on — and the Epomaker Glyph is a sheer delight. No word of a lie, this is <em>the </em>best keyboard I've typed on so far. It comes fitted with pre-lubed Epomaker Wisteria V2 linear switches which require just 38g of force to activate, and have a pre-travel distance of 2mm (total 3.6mm). Thanks to these specs, you don't need to press them all the way down to activate them, which means that you can type quickly.</p><p>And that's exactly what I've been doing on the Glyph. Call me a speed demon because I've been able to achieve 109 words per minute with 93.2% accuracy — and that's a new personal best for me. I love everything about typing on the Glyph: the double-shot PBT keycaps, the type-feel, and most of all, the <em>suuuuuper </em>creamy sound profile. It's delicious.</p><p>As mechanical keyboards go, the Glyph is downright fantastic to type on. It's hot-swappable too so if you aren't a fan of the stock switches (which I'd find surprising), you can grab any south-facing 3- and 5-pin MX switches and pop them in without de- and re-soldering them to the PCB.</p><div data-widget-type="review" data-model-name="Epomaker Glyph"></div><h2 id="comfort-for-days">Comfort for days</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7870px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nRE477cdhXmZMuKtHniAAN" name="Epomaker_Glyph" alt="Epomaker Glyph's wrist rest" src="https://cdn.mos.cms.futurecdn.net/nRE477cdhXmZMuKtHniAAN-1920-80.jpg" mos="" align="middle" fullscreen="" width="7870" height="4427" attribution="" endorsement="" class="inline"></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 love it when keyboards, mice or any other peripherals come with accessories, and the Epomaker Glyph is accompanied by my most favorite yet. Included in the box is a CloudGel wrist rest that's made of high-quality silicone and PVC materials, and it's so soft to rest my wrists on. It packs gentle cushioning which alleviates any pressure I would have felt otherwise.</p><p>The wrist rest also features a slow-rebound gel design which deforms according to the pressure and shape of your wrists. Your hands then feel comfortable, even when you're working long hours. I've typed extensively on the Glyph and I haven't felt any discomfort. Even if you don't want to get the Glyph (but you <em>really </em>should), you can grab the wrist rest separately for <a href="https://epomaker.com/products/epomaker-cloudgel-wrist-rest" target="_blank" rel="nofollow">$24 / £18</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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CfTyBr5QuLHymePzaZYCGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CfTyBr5QuLHymePzaZYCGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mechanical keyboard that looks ridiculously good, packs plenty of useful modern features, sounds like music to your ears, and is a joy to type on, few are better than the Glyph. I could wax lyrical about the keyboard all day long, but you can also read my full <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph review</a> for that.</p><p>I can't believe it took this long to find my dream keyboard, but the Glyph was certainly worth the wait.</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/computing/keyboards/epomaker-glyph-review">‘The most absurd and wonderful keyboard I’ve ever typed on’: I tested the typewriter-style Epomaker Glyph and it’s earned a permanent spot on my desk</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li><li><a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">The best mechanical keyboards in 2026</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/ive-tested-nearly-100-keyboards-and-finally-found-my-dream-deck-heres-why-the-epomaker-glyph-is-my-new-favorite</link>
                                                                            <description>
                            <![CDATA[ For the last two weeks, I've been typing on the typewriter-style Epomaker Glyph, and its sound, design and typing experience have truly won me over. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">QVqNoKpUHCKyThzoRdaUXo</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/qbniJkrVVRJTNn875qprGg-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 13 Jun 2026 09:45:00 +0000</pubDate>                                                                                                                                <updated>Sun, 14 Jun 2026 19:02:00 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/qbniJkrVVRJTNn875qprGg-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[An Epomaker Glyph mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[An Epomaker Glyph mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[An Epomaker Glyph mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/qbniJkrVVRJTNn875qprGg-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I never thought the words "my dream keyboard" would come out of my mouth. Two and a half years ago, I didn't really care much about keyboards — and I was then introduced to the vast world of mechanical and Hall Effect decks. Things haven't been the same ever since. I've come to realize that a good keyboard can really uplift your mood, and make you a better typist — and as someone whose bread and butter depends on typing, I <em>need </em>a powerful deck.</p><p>I've reviewed nearly 100 keyboards during those last two and a half years, and I think my search for the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> is finally over. I've spent the last two weeks typing on the typewriter-style <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph</a>, and it upstages every other productivity keyboard in more ways than one.</p><div class="product"><a data-dimension112="91c057b5-f0bd-448e-a5a0-903b44b62ba3" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$132" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1415px;"><p class="vanilla-image-block" style="padding-top:59.72%;"><img id="Eg3Dv2pDn9hbKqxeMTCRgW" name="Epo-Glyph-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Eg3Dv2pDn9hbKqxeMTCRgW-1920-80.jpg" mos="" align="middle" fullscreen="" width="1415" height="845" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Epomaker Glyph is the most fun mechanical keyboard I’ve had the pleasure of testing. Packing responsive, light Wisteria V2 linear switches and boasting a creamy and buttery  sound profile, the Glyph puts the joy back into typing. The keyboard sports a charming typewriter-like design with dual screens and customizable RGB lighting, and build quality is solid too.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow" data-dimension112="91c057b5-f0bd-448e-a5a0-903b44b62ba3" data-action="Deal Block" data-label="Glyph" data-dimension48="Glyph" data-dimension25="$132">View Deal</a></p></div><p>The Glyph is perhaps one of the best built and most beautifully designed keyboards I've ever tested. Its rounded, typewriter-like keycaps and creamy sound profile are sure to draw eyeballs to your desk — in the best way possible. Dual screens that show the board's vitals (or GIFs) are a nice touch, and honestly, there's nothing I'd change about the Glyph. Here's why it's my dream keyboard.</p><h2 id="drool-worthy-design">Drool-worthy 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="BaY3N4p2Sup3v7bcrdva5g" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/BaY3N4p2Sup3v7bcrdva5g-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 so much to love about the Epomaker Glyph, and its typewriter-like design is one of its most striking features. The Glyph's body is covered in Retro White while its rounded keycaps utilize a combination of sage green and off-white. It's soft and glossy to look at and touch, and when you throw in south-facing RGB lighting as well as an ambient light strip that falls on the surface the keyboard is sitting on, you've got yourself a winner.</p><p>The Glyph isn't a "typewriter-style" keyboard only by name, and it's got a specific typewriter-like function to prove it. A return lever is located in the top-left corner which acts as the Enter key when pushed up and Backspace when pulled down. I know that you're thinking that's so gimmicky, and I thought the same when I first saw the keyboard — but I've been using it every so often while typing, and it's great fun.</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:400px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hMd6veZFaS8PiCtJt6wic6" name="Epomaker-Glyph" alt="A GIF of puffings displayed on an Epomaker Glyph keyboard" src="https://cdn.mos.cms.futurecdn.net/hMd6veZFaS8PiCtJt6wic6-1920-80.gif" mos="" align="middle" fullscreen="" width="400" height="225" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: National Geographic / Tom's Guide)</span></figcaption></figure><p>What I love about the Glyph is its dual screens which help it strike a balance between modern functionality and retro aesthetics. These screens display the date and time, and the keyboard's vitals, such as battery life, connection mode, active operating system, CPU usage, and more. Or you could display a GIF of puffins, like I did!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-XmAwEX"></div>                            </div>                            <script src="https://kwizly.com/embed/XmAwEX.js" async></script><h2 id="call-me-a-speed-demon">Call me a speed demon</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="74p25fJhGbVJscmLFXa28f" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/74p25fJhGbVJscmLFXa28f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>A keyboard's looks mean nothing if it isn't good to type on — and the Epomaker Glyph is a sheer delight. No word of a lie, this is <em>the </em>best keyboard I've typed on so far. It comes fitted with pre-lubed Epomaker Wisteria V2 linear switches which require just 38g of force to activate, and have a pre-travel distance of 2mm (total 3.6mm). Thanks to these specs, you don't need to press them all the way down to activate them, which means that you can type quickly.</p><p>And that's exactly what I've been doing on the Glyph. Call me a speed demon because I've been able to achieve 109 words per minute with 93.2% accuracy — and that's a new personal best for me. I love everything about typing on the Glyph: the double-shot PBT keycaps, the type-feel, and most of all, the <em>suuuuuper </em>creamy sound profile. It's delicious.</p><p>As mechanical keyboards go, the Glyph is downright fantastic to type on. It's hot-swappable too so if you aren't a fan of the stock switches (which I'd find surprising), you can grab any south-facing 3- and 5-pin MX switches and pop them in without de- and re-soldering them to the PCB.</p><div data-widget-type="review" data-model-name="Epomaker Glyph"></div><h2 id="comfort-for-days">Comfort for days</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7870px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nRE477cdhXmZMuKtHniAAN" name="Epomaker_Glyph" alt="Epomaker Glyph's wrist rest" src="https://cdn.mos.cms.futurecdn.net/nRE477cdhXmZMuKtHniAAN-1920-80.jpg" mos="" align="middle" fullscreen="" width="7870" height="4427" attribution="" endorsement="" class="inline"></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 love it when keyboards, mice or any other peripherals come with accessories, and the Epomaker Glyph is accompanied by my most favorite yet. Included in the box is a CloudGel wrist rest that's made of high-quality silicone and PVC materials, and it's so soft to rest my wrists on. It packs gentle cushioning which alleviates any pressure I would have felt otherwise.</p><p>The wrist rest also features a slow-rebound gel design which deforms according to the pressure and shape of your wrists. Your hands then feel comfortable, even when you're working long hours. I've typed extensively on the Glyph and I haven't felt any discomfort. Even if you don't want to get the Glyph (but you <em>really </em>should), you can grab the wrist rest separately for <a href="https://epomaker.com/products/epomaker-cloudgel-wrist-rest" target="_blank" rel="nofollow">$24 / £18</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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="CfTyBr5QuLHymePzaZYCGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CfTyBr5QuLHymePzaZYCGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 mechanical keyboard that looks ridiculously good, packs plenty of useful modern features, sounds like music to your ears, and is a joy to type on, few are better than the Glyph. I could wax lyrical about the keyboard all day long, but you can also read my full <a href="https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review">Epomaker Glyph review</a> for that.</p><p>I can't believe it took this long to find my dream keyboard, but the Glyph was certainly worth the wait.</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/computing/keyboards/epomaker-glyph-review">‘The most absurd and wonderful keyboard I’ve ever typed on’: I tested the typewriter-style Epomaker Glyph and it’s earned a permanent spot on my desk</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li><li><a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">The best mechanical keyboards in 2026</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I thought I was using the greatest webcam in my home office, but after testing the Insta360 Link 2C Pro, it’s changed everything ]]></title>
                                                                                                <dc:content><![CDATA[ <p>If you’re looking for the <a href="https://www.tomsguide.com/computing/peripherals/best-webcams">best webcam</a> to upgrade your home office, look no further than the Insta360 Link 2C Pro. When I reviewed the Insta360 Link 2 Pro (just one letter in the name and a built-in gimbal sets these two apart) a few months ago, I thought the gimbal made it the obvious choice, but how wrong I was — the Insta360 Link 2C Pro is $50 cheaper and can do everything the gimbal version can do.</p><p>The Insta360 Link 2C Pro has all of the same perks, including dual native ISO that is effective even in the harshest lighting conditions, and a privacy switch for easy manual control. What makes the Link 2C stand out is the smart modes, which make presenting a breeze, and the AI tracking makes me wonder why we ever needed a gimbal version of this camera. </p><p>To find out more about the webcam, you can keep reading this full Insta360 Link 2C Pro review.</p><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-cheat-sheet"><span>Insta360 Link 2C Pro review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 4K/30fps webcam suited with smart modes to make presenting simple</li><li><strong>Who is it for? </strong>At-home office workers looking to upgrade their video conferencing setup</li><li><strong>What does it cost? </strong>The Insta360 Link 2C Pro is available for <a href="https://www.amazon.com/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">£199</a> at Amazon</li><li><strong>What do we like?</strong> Good value, dual native ISO for low-light, privacy switch for security, smart modes</li><li><strong>What don’t we like? It doesn’t have a gimbal for auto-tracking</strong></li></ul><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-specs"><span>Insta360 Link 2C Pro 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/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">£199</a></p></td></tr><tr><td class="firstcol " ><p><strong>Compatibility</strong></p></td><td  ><p>Windows 10 or later, macOS 10.13 or later</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>USB-C cable, USB-A adapter</p></td></tr><tr><td class="firstcol " ><p><strong>Digital zoom</strong></p></td><td  ><p>Up to 4x</p></td></tr><tr><td class="firstcol " ><p><strong>Sensor size</strong></p></td><td  ><p>1/1.3 inch</p></td></tr><tr><td class="firstcol " ><p><strong>Gimbal</strong></p></td><td  ><p>None</p></td></tr><tr><td class="firstcol " ><p><strong>Max landscape video resolution</strong></p></td><td  ><p>4K/30fps</p><p>1080p/60fps</p><p>720p/60fps</p><p>360p/30fps</p></td></tr><tr><td class="firstcol " ><p><strong>Max portrait video resolution</strong></p></td><td  ><p>4K/30fps</p><p>1080p/60fps</p><p>720p/60fps</p></td></tr><tr><td class="firstcol " ><p><strong>Exposure compensation</strong></p></td><td  ><p>±3 EV</p></td></tr><tr><td class="firstcol " ><p><strong>ISO</strong></p></td><td  ><p>Dual Native ISO 100-3200</p></td></tr><tr><td class="firstcol " ><p><strong>Shutter speed</strong></p></td><td  ><p>1/8000s to 1/30s</p></td></tr><tr><td class="firstcol " ><p><strong>White balance</strong></p></td><td  ><p>2000K-10,000K</p></td></tr><tr><td class="firstcol " ><p><strong>Aperture</strong></p></td><td  ><p>f/1.9</p></td></tr><tr><td class="firstcol " ><p><strong>Autofocus</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Minimum focusing distance</strong></p></td><td  ><p>12cm</p></td></tr><tr><td class="firstcol " ><p><strong>HDR</strong></p></td><td  ><p>Supported</p></td></tr><tr><td class="firstcol " ><p><strong>Number of microphones</strong></p></td><td  ><p>2</p></td></tr><tr><td class="firstcol " ><p><strong>Noise reduction</strong></p></td><td  ><p>AI Noise-canceling</p></td></tr><tr><td class="firstcol " ><p><strong>Audio modes</strong></p></td><td  ><p>Standard, Wide Focus, Original</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>2.5 x 1.2 x 1 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight without magnetic mount</strong></p></td><td  ><p>1.71 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Weight with magnetic mount</strong></p></td><td  ><p>4 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Color</strong></p></td><td  ><p>Graphite black</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-the-ups"><span>Insta360 Link 2C Pro review: The ups</span></h2><p>There’s a lot to love about the Insta360 Link 2C Pro. You get all the perks of the Insta360 Link 2 Pro at a fraction of the price. The image performance is sharp even in low light conditions thanks to dual native ISO, and there are plenty of modes to streamline usage for presenting. </p><h2 id="save-money-and-get-the-same-specs">Save money and get the same specs</h2><p>The Insta360 Link 2C Pro is virtually the same camera as the Insta360 Link 2 Pro. Eagle-eyed readers among you will notice only a one-letter difference in the names, which is admittedly confusing. The only functional difference between them is that the Link 2C lacks a 2-axis gimbal for tracking moving subjects. </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="fFD98QzpiGf8yhK8sTE8qd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/fFD98QzpiGf8yhK8sTE8qd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 gimbal tracking isn’t something every user needs, and for most workers looking for a camera to support video streaming and work calls, a stationary one is more than enough. You also still get all of the smart modes, same image performance, and same ISO capabilities. </p><p>Unless you’re planning to use the camera for presentations, it’s well worth saving $50 by purchasing the Link 2C Pro. If you’re currently using the older <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-review">Link 2C</a> ($149), it’s worth considering the upgrade to the Link 2C Pro for the larger sensor and better low-light performance. </p><h2 id="image-performance">Image performance</h2><p>The image of the Insta360 Link 2C Pro is a lot sharper than my usual <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a>’s built-in camera. The camera has a max resolution of 4K/30fps, which meant that I always looked clear and my movements always appeared fairly smooth. Since you’re likely not using your webcam for anything more advanced than a video call, 30fps is more than enough. </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="pz2GwpDFiVjbtd4MgLY32e" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/pz2GwpDFiVjbtd4MgLY32e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 4K resolution also meant that I could see my face in detail, which on a no-makeup day isn’t the best news for me (or anyone watching me for that matter). Thankfully, with the companion software downloaded, you can alter the video resolution. You can also use the camera in 1080p/60fps, 720p/60fps or 360p/30fps, depending on your needs. If you want footage free from blur, 60fps is better to capture faster movements, but as I said, 30fps is likely plenty for work use. </p><p>My day-to-day webcam is the built-in unit on the MacBook Pro M2, so when I first used the Link 2C Pro, I was taken aback by how pale I looked. Whereas Mac cameras have a warmth to them, the Insta360 is very cool-toned. This won’t be for everyone, but you can make adjustments to the color hue by using a filter in the companion software, Link Connect. </p><h2 id="privacy-switch">Privacy switch</h2><p>On the left-hand side of the webcam, there's a handy switch for closing the privacy lens on the camera. This is very useful for the privacy-conscious, but also in the case of temperamental technology. I’ve found myself in video meetings where my on-screen controls are unresponsive because I’ve got too many tabs open or I’m running too many applications at once, meaning I can’t turn off my camera. This is a great tool to quickly switch your camera off without fuss. </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="8ZEb3RNvzf2F8ZVcjS974e" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/8ZEb3RNvzf2F8ZVcjS974e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 privacy switch is something you don’t get with the Insta360 Link 2 Pro — since it’s moving constantly on a gimbal, it would be tricky to use a physical control like this.</p><h2 id="easy-setup">Easy setup</h2><p>The Insta360 Link 2C Pro is incredibly easy to set up. The adjustable mount can attach to most desktop monitors and even fit securely to the screen of my MacBook, so it can cope with being attached to a thin screen. The camera attaches magnetically to the mount, so there’s no fussing with slots or sockets. You tap them together and pull them apart, making transporting and storing it very easy. </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="SYFUzzYvDSrqCPyScfW7Zd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/SYFUzzYvDSrqCPyScfW7Zd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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/computing/peripherals/osbot-meet-2-webcam-review">Osbot Meet 2</a> ($129) also uses a magnetic clip-on mount, but has a tendency to fall off from time to time. I didn’t have this issue with the Link 2C Pro.</p><p>The camera is powered with a USB-C cable, which plugs into the device you're using to connect. The camera also comes with a USB-C to USB-A adaptor. This was just as easy to set up as the <a href="https://www.tomsguide.com/computing/peripherals/osbot-meet-4k-webcam-review">Osbot Meet 4K</a> ($149), which also uses a USB-C cable. </p><h2 id="useful-modes">Useful modes</h2><p>The Insta360 Link 2C Pro has a lot of smart features available through the companion software, Insta360 Link Connect. This is available on macOS and Windows and takes a few minutes to download to your device, giving you access to the webcam settings, including firmware updates.</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="fFD98QzpiGf8yhK8sTE8qd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/fFD98QzpiGf8yhK8sTE8qd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 is a great selection of modes on the camera to help with professional presenting. These include AI framing, Smart Whiteboard, DeskView and Virtual Background. </p><h3 id="ai-framing">AI framing</h3><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/CXvwCZCBuzgqCcyRY7QpvZ/Ai%20tracking.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/CXvwCZCBuzgqCcyRY7QpvZ/Ai%20tracking.mp4"></video></div><p>Although the webcam doesn’t have gimbal tracking, it does have AI framing. This doesn’t move to track but rather zooms and reframes to make sure everyone is in the frame. To test this out, I recruited my colleagues Nikita and Harry, and as you can see, the camera did a good job at accounting for all three of us and our varying heights.</p><h3 id="smart-whiteboard">Smart whiteboard</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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="mmDnFR4ZtLFNV9F96rjQP5" name="Insta360 Link 2C Pro" alt="Image of a whiteboard captured using the Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/mmDnFR4ZtLFNV9F96rjQP5-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The smart whiteboard feature was very handy when using the camera, as it zooms in to focus on the board after scanning the screen and detecting a whiteboard. It’s ideal for presenting to minimize distractions around the scene.</p><h3 id="deskview">DeskView</h3><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/yGdna5Ey6MxBGXZiWLmNy7/DeskView.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/yGdna5Ey6MxBGXZiWLmNy7/DeskView.mp4"></video></div><p>The webcam also has DeskView, which, when enabled, flips the view of the camera. This means that you can view and present whatever is on your desk without having to read or work upside down for the benefit of the camera.</p><h3 id="virtual-background">Virtual Background</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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7uL6aMJ9cE9nLdb9FZGow9" name="Insta360 Link 2C Pro" alt="Virtual background settings on the Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/7uL6aMJ9cE9nLdb9FZGow9-1920-80.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: Tom's Guide)</span></figcaption></figure><p>You can also use a virtual background in the Link Connect app. You could do this in whatever meeting software you use, like Google Meet or Teams, but I think the Insta360 did a good job at cutting around the finer details like the wispy layers of my hair. </p><h2 id="dual-native-iso">Dual Native ISO</h2><p>The 1/1.3-inch sensor of the Insta360 Link 2C Pro has dual native ISO (where the camera switches between two native ISO ranges to get better results in extreme light or dark lighting), so even in the dark, such as in the image below, I was able to get more of a natural lighting experience. </p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/rjCGhuYR5a6qoLMK8kgSaF-1920-80.jpg" alt="Dual Native ISO on Insta360 Link 2C Pro" /><figcaption><small role="credit">Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/RpQuBuGjb4PYJLqUxzGHcF-1920-80.jpg" alt="Dual Native ISO on Insta360 Link 2C Pro" /><figcaption><small role="credit">Tom's Guide</small></figcaption></figure></figure><p>There isn’t too much noise in the image. It still looks like I’m sitting in the dark, obviously, but it brings back some detail in the frame by lifting the shadows. </p><h2 id="good-microphone-2">Good microphone</h2><p>There wasn’t much difference in my voice when using my MacBook's native microphone and the microphone of the Insta360 Link 2C Pro. </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="y64vdLNuaCh8NuXBUDYH7e" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/y64vdLNuaCh8NuXBUDYH7e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 voice was clear, and it did a good job of minimizing typing sounds when I was speaking on a call with my colleagues. </p><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-the-downs"><span>Insta360 Link 2C Pro review: The downs</span></h2><p>Unless you need a gimbal, the Insta360 Link 2C Pro is virtually perfect. But if it’s across-the-room tracking you need, then you may struggle with this webcam. </p><h2 id="no-gimbal">No gimbal </h2><p>As I mentioned earlier, what makes the Insta360 Link 2C Pro camera so great is that you get all the perks of a flagship webcam without the stark price of the Insta360 Link 2 Pro. But in opting for the cheaper option, you are trading access to a gimbal. </p><p>One of the things I loved so much about the Insta360 Link 2 Pro was that it followed me wherever I was in the frame, making it an ideal option for recording wider scenes, like presentations. </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="4gFJhk7RmmYqYwuRSFhbVd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/4gFJhk7RmmYqYwuRSFhbVd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>By contrast, the Link 2C Pro is stationary, so you’re a bit more limited. But this is still fine for office workers who take meetings at their desks and don’t need movement tracking across a wide stage. </p><p>The Link 2C Pro has AI tracking, which I mentioned above. This zooms in to keep tracking movements, losing resolution. The Link 2 Pro doesn’t lose any resolution thanks to the gimbal.</p><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-verdict"><span>Insta360 Link 2C Pro review: Verdict</span></h2><p>I think the Insta360 Link 2C Pro is the best webcam option for office workers looking to upgrade their video call setup without breaking the bank. You get all the smart features of the Insta360 Link 2 Pro, just without the gimbal — but if you’re not walking around and presenting while on a call, you really don’t need it.  Do you want to be in frame at all times? It’s still no problem with the Link 2C Pro because the AI tracking is just as effective at keeping you and anyone else around you in the frame (albeit at the cost of resolution). </p><p>The dual native ISO works well in low-light situations, lifting shadows without grain. The privacy switch is also handy, especially if you frequently need to quickly make camera adjustments. </p><p>As I’ve mentioned, the lack of a gimbal could be a deal-breaker for some, but I think it’s worth the sacrifice to save yourself $50. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/insta360-link-2c-pro-review</link>
                                                                            <description>
                            <![CDATA[ The Insta360 Link 2C Pro webcam is a worthy upgrade for the home office that keeps everyone in frame even without a gimbal. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">QSuNh6kCygL8aLHmsBqiCE</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/EqZ6DkhuoJB89GLgyANqed-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 12 Jun 2026 11:45:32 +0000</pubDate>                                                                                                                                <updated>Tue, 16 Jun 2026 16:05:28 +0000</updated>
                                                                                                                                            <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></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-320-70.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/EqZ6DkhuoJB89GLgyANqed-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Insta360 Link 2C Pro]]></media:description>                                                            <media:text><![CDATA[Insta360 Link 2C Pro]]></media:text>
                                <media:title type="plain"><![CDATA[Insta360 Link 2C Pro]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/EqZ6DkhuoJB89GLgyANqed-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>If you’re looking for the <a href="https://www.tomsguide.com/computing/peripherals/best-webcams">best webcam</a> to upgrade your home office, look no further than the Insta360 Link 2C Pro. When I reviewed the Insta360 Link 2 Pro (just one letter in the name and a built-in gimbal sets these two apart) a few months ago, I thought the gimbal made it the obvious choice, but how wrong I was — the Insta360 Link 2C Pro is $50 cheaper and can do everything the gimbal version can do.</p><p>The Insta360 Link 2C Pro has all of the same perks, including dual native ISO that is effective even in the harshest lighting conditions, and a privacy switch for easy manual control. What makes the Link 2C stand out is the smart modes, which make presenting a breeze, and the AI tracking makes me wonder why we ever needed a gimbal version of this camera. </p><p>To find out more about the webcam, you can keep reading this full Insta360 Link 2C Pro review.</p><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-cheat-sheet"><span>Insta360 Link 2C Pro review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 4K/30fps webcam suited with smart modes to make presenting simple</li><li><strong>Who is it for? </strong>At-home office workers looking to upgrade their video conferencing setup</li><li><strong>What does it cost? </strong>The Insta360 Link 2C Pro is available for <a href="https://www.amazon.com/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">£199</a> at Amazon</li><li><strong>What do we like?</strong> Good value, dual native ISO for low-light, privacy switch for security, smart modes</li><li><strong>What don’t we like? It doesn’t have a gimbal for auto-tracking</strong></li></ul><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-specs"><span>Insta360 Link 2C Pro 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/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">$199</a> / <a href="https://www.amazon.co.uk/Insta360-Link-Pro-Directional-Noise-Canceling/dp/B0G3SSQMJQ" target="_blank" rel="nofollow">£199</a></p></td></tr><tr><td class="firstcol " ><p><strong>Compatibility</strong></p></td><td  ><p>Windows 10 or later, macOS 10.13 or later</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>USB-C cable, USB-A adapter</p></td></tr><tr><td class="firstcol " ><p><strong>Digital zoom</strong></p></td><td  ><p>Up to 4x</p></td></tr><tr><td class="firstcol " ><p><strong>Sensor size</strong></p></td><td  ><p>1/1.3 inch</p></td></tr><tr><td class="firstcol " ><p><strong>Gimbal</strong></p></td><td  ><p>None</p></td></tr><tr><td class="firstcol " ><p><strong>Max landscape video resolution</strong></p></td><td  ><p>4K/30fps</p><p>1080p/60fps</p><p>720p/60fps</p><p>360p/30fps</p></td></tr><tr><td class="firstcol " ><p><strong>Max portrait video resolution</strong></p></td><td  ><p>4K/30fps</p><p>1080p/60fps</p><p>720p/60fps</p></td></tr><tr><td class="firstcol " ><p><strong>Exposure compensation</strong></p></td><td  ><p>±3 EV</p></td></tr><tr><td class="firstcol " ><p><strong>ISO</strong></p></td><td  ><p>Dual Native ISO 100-3200</p></td></tr><tr><td class="firstcol " ><p><strong>Shutter speed</strong></p></td><td  ><p>1/8000s to 1/30s</p></td></tr><tr><td class="firstcol " ><p><strong>White balance</strong></p></td><td  ><p>2000K-10,000K</p></td></tr><tr><td class="firstcol " ><p><strong>Aperture</strong></p></td><td  ><p>f/1.9</p></td></tr><tr><td class="firstcol " ><p><strong>Autofocus</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Minimum focusing distance</strong></p></td><td  ><p>12cm</p></td></tr><tr><td class="firstcol " ><p><strong>HDR</strong></p></td><td  ><p>Supported</p></td></tr><tr><td class="firstcol " ><p><strong>Number of microphones</strong></p></td><td  ><p>2</p></td></tr><tr><td class="firstcol " ><p><strong>Noise reduction</strong></p></td><td  ><p>AI Noise-canceling</p></td></tr><tr><td class="firstcol " ><p><strong>Audio modes</strong></p></td><td  ><p>Standard, Wide Focus, Original</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>2.5 x 1.2 x 1 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight without magnetic mount</strong></p></td><td  ><p>1.71 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Weight with magnetic mount</strong></p></td><td  ><p>4 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Color</strong></p></td><td  ><p>Graphite black</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-the-ups"><span>Insta360 Link 2C Pro review: The ups</span></h2><p>There’s a lot to love about the Insta360 Link 2C Pro. You get all the perks of the Insta360 Link 2 Pro at a fraction of the price. The image performance is sharp even in low light conditions thanks to dual native ISO, and there are plenty of modes to streamline usage for presenting. </p><h2 id="save-money-and-get-the-same-specs">Save money and get the same specs</h2><p>The Insta360 Link 2C Pro is virtually the same camera as the Insta360 Link 2 Pro. Eagle-eyed readers among you will notice only a one-letter difference in the names, which is admittedly confusing. The only functional difference between them is that the Link 2C lacks a 2-axis gimbal for tracking moving subjects. </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="fFD98QzpiGf8yhK8sTE8qd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/fFD98QzpiGf8yhK8sTE8qd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 gimbal tracking isn’t something every user needs, and for most workers looking for a camera to support video streaming and work calls, a stationary one is more than enough. You also still get all of the smart modes, same image performance, and same ISO capabilities. </p><p>Unless you’re planning to use the camera for presentations, it’s well worth saving $50 by purchasing the Link 2C Pro. If you’re currently using the older <a href="https://www.tomsguide.com/computing/peripherals/insta360-link-2c-review">Link 2C</a> ($149), it’s worth considering the upgrade to the Link 2C Pro for the larger sensor and better low-light performance. </p><h2 id="image-performance">Image performance</h2><p>The image of the Insta360 Link 2C Pro is a lot sharper than my usual <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a>’s built-in camera. The camera has a max resolution of 4K/30fps, which meant that I always looked clear and my movements always appeared fairly smooth. Since you’re likely not using your webcam for anything more advanced than a video call, 30fps is more than enough. </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="pz2GwpDFiVjbtd4MgLY32e" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/pz2GwpDFiVjbtd4MgLY32e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 4K resolution also meant that I could see my face in detail, which on a no-makeup day isn’t the best news for me (or anyone watching me for that matter). Thankfully, with the companion software downloaded, you can alter the video resolution. You can also use the camera in 1080p/60fps, 720p/60fps or 360p/30fps, depending on your needs. If you want footage free from blur, 60fps is better to capture faster movements, but as I said, 30fps is likely plenty for work use. </p><p>My day-to-day webcam is the built-in unit on the MacBook Pro M2, so when I first used the Link 2C Pro, I was taken aback by how pale I looked. Whereas Mac cameras have a warmth to them, the Insta360 is very cool-toned. This won’t be for everyone, but you can make adjustments to the color hue by using a filter in the companion software, Link Connect. </p><h2 id="privacy-switch">Privacy switch</h2><p>On the left-hand side of the webcam, there's a handy switch for closing the privacy lens on the camera. This is very useful for the privacy-conscious, but also in the case of temperamental technology. I’ve found myself in video meetings where my on-screen controls are unresponsive because I’ve got too many tabs open or I’m running too many applications at once, meaning I can’t turn off my camera. This is a great tool to quickly switch your camera off without fuss. </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="8ZEb3RNvzf2F8ZVcjS974e" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/8ZEb3RNvzf2F8ZVcjS974e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 privacy switch is something you don’t get with the Insta360 Link 2 Pro — since it’s moving constantly on a gimbal, it would be tricky to use a physical control like this.</p><h2 id="easy-setup">Easy setup</h2><p>The Insta360 Link 2C Pro is incredibly easy to set up. The adjustable mount can attach to most desktop monitors and even fit securely to the screen of my MacBook, so it can cope with being attached to a thin screen. The camera attaches magnetically to the mount, so there’s no fussing with slots or sockets. You tap them together and pull them apart, making transporting and storing it very easy. </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="SYFUzzYvDSrqCPyScfW7Zd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/SYFUzzYvDSrqCPyScfW7Zd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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/computing/peripherals/osbot-meet-2-webcam-review">Osbot Meet 2</a> ($129) also uses a magnetic clip-on mount, but has a tendency to fall off from time to time. I didn’t have this issue with the Link 2C Pro.</p><p>The camera is powered with a USB-C cable, which plugs into the device you're using to connect. The camera also comes with a USB-C to USB-A adaptor. This was just as easy to set up as the <a href="https://www.tomsguide.com/computing/peripherals/osbot-meet-4k-webcam-review">Osbot Meet 4K</a> ($149), which also uses a USB-C cable. </p><h2 id="useful-modes">Useful modes</h2><p>The Insta360 Link 2C Pro has a lot of smart features available through the companion software, Insta360 Link Connect. This is available on macOS and Windows and takes a few minutes to download to your device, giving you access to the webcam settings, including firmware updates.</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="fFD98QzpiGf8yhK8sTE8qd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/fFD98QzpiGf8yhK8sTE8qd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 is a great selection of modes on the camera to help with professional presenting. These include AI framing, Smart Whiteboard, DeskView and Virtual Background. </p><h3 id="ai-framing">AI framing</h3><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/CXvwCZCBuzgqCcyRY7QpvZ/Ai%20tracking.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/CXvwCZCBuzgqCcyRY7QpvZ/Ai%20tracking.mp4"></video></div><p>Although the webcam doesn’t have gimbal tracking, it does have AI framing. This doesn’t move to track but rather zooms and reframes to make sure everyone is in the frame. To test this out, I recruited my colleagues Nikita and Harry, and as you can see, the camera did a good job at accounting for all three of us and our varying heights.</p><h3 id="smart-whiteboard">Smart whiteboard</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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="mmDnFR4ZtLFNV9F96rjQP5" name="Insta360 Link 2C Pro" alt="Image of a whiteboard captured using the Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/mmDnFR4ZtLFNV9F96rjQP5-1920-80.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: Tom's Guide)</span></figcaption></figure><p>The smart whiteboard feature was very handy when using the camera, as it zooms in to focus on the board after scanning the screen and detecting a whiteboard. It’s ideal for presenting to minimize distractions around the scene.</p><h3 id="deskview">DeskView</h3><div class="looped-video"><video class="lazyload-in-view lazyloading" data-src="https://cdn.mos.cms.futurecdn.net/yGdna5Ey6MxBGXZiWLmNy7/DeskView.mp4" autoplay loop muted playsinline src="https://cdn.mos.cms.futurecdn.net/yGdna5Ey6MxBGXZiWLmNy7/DeskView.mp4"></video></div><p>The webcam also has DeskView, which, when enabled, flips the view of the camera. This means that you can view and present whatever is on your desk without having to read or work upside down for the benefit of the camera.</p><h3 id="virtual-background">Virtual Background</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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7uL6aMJ9cE9nLdb9FZGow9" name="Insta360 Link 2C Pro" alt="Virtual background settings on the Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/7uL6aMJ9cE9nLdb9FZGow9-1920-80.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: Tom's Guide)</span></figcaption></figure><p>You can also use a virtual background in the Link Connect app. You could do this in whatever meeting software you use, like Google Meet or Teams, but I think the Insta360 did a good job at cutting around the finer details like the wispy layers of my hair. </p><h2 id="dual-native-iso">Dual Native ISO</h2><p>The 1/1.3-inch sensor of the Insta360 Link 2C Pro has dual native ISO (where the camera switches between two native ISO ranges to get better results in extreme light or dark lighting), so even in the dark, such as in the image below, I was able to get more of a natural lighting experience. </p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/rjCGhuYR5a6qoLMK8kgSaF-1920-80.jpg" alt="Dual Native ISO on Insta360 Link 2C Pro" /><figcaption><small role="credit">Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/RpQuBuGjb4PYJLqUxzGHcF-1920-80.jpg" alt="Dual Native ISO on Insta360 Link 2C Pro" /><figcaption><small role="credit">Tom's Guide</small></figcaption></figure></figure><p>There isn’t too much noise in the image. It still looks like I’m sitting in the dark, obviously, but it brings back some detail in the frame by lifting the shadows. </p><h2 id="good-microphone-2">Good microphone</h2><p>There wasn’t much difference in my voice when using my MacBook's native microphone and the microphone of the Insta360 Link 2C Pro. </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="y64vdLNuaCh8NuXBUDYH7e" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/y64vdLNuaCh8NuXBUDYH7e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 voice was clear, and it did a good job of minimizing typing sounds when I was speaking on a call with my colleagues. </p><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-the-downs"><span>Insta360 Link 2C Pro review: The downs</span></h2><p>Unless you need a gimbal, the Insta360 Link 2C Pro is virtually perfect. But if it’s across-the-room tracking you need, then you may struggle with this webcam. </p><h2 id="no-gimbal">No gimbal </h2><p>As I mentioned earlier, what makes the Insta360 Link 2C Pro camera so great is that you get all the perks of a flagship webcam without the stark price of the Insta360 Link 2 Pro. But in opting for the cheaper option, you are trading access to a gimbal. </p><p>One of the things I loved so much about the Insta360 Link 2 Pro was that it followed me wherever I was in the frame, making it an ideal option for recording wider scenes, like presentations. </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="4gFJhk7RmmYqYwuRSFhbVd" name="Insta360 Link 2C Pro" alt="Insta360 Link 2C Pro" src="https://cdn.mos.cms.futurecdn.net/4gFJhk7RmmYqYwuRSFhbVd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>By contrast, the Link 2C Pro is stationary, so you’re a bit more limited. But this is still fine for office workers who take meetings at their desks and don’t need movement tracking across a wide stage. </p><p>The Link 2C Pro has AI tracking, which I mentioned above. This zooms in to keep tracking movements, losing resolution. The Link 2 Pro doesn’t lose any resolution thanks to the gimbal.</p><h2 class="article-body__section" id="section-insta360-link-2c-pro-review-verdict"><span>Insta360 Link 2C Pro review: Verdict</span></h2><p>I think the Insta360 Link 2C Pro is the best webcam option for office workers looking to upgrade their video call setup without breaking the bank. You get all the smart features of the Insta360 Link 2 Pro, just without the gimbal — but if you’re not walking around and presenting while on a call, you really don’t need it.  Do you want to be in frame at all times? It’s still no problem with the Link 2C Pro because the AI tracking is just as effective at keeping you and anyone else around you in the frame (albeit at the cost of resolution). </p><p>The dual native ISO works well in low-light situations, lifting shadows without grain. The privacy switch is also handy, especially if you frequently need to quickly make camera adjustments. </p><p>As I’ve mentioned, the lack of a gimbal could be a deal-breaker for some, but I think it’s worth the sacrifice to save yourself $50. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ ‘The most absurd and wonderful keyboard I’ve ever typed on’: I tested the typewriter-style Epomaker Glyph and it’s earned a permanent spot on my desk ]]></title>
                                                                                                <dc:content><![CDATA[ <p>As someone whose bread and butter revolves around typing, I need one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> to get the job done. Mech decks can be fun, but few are as fun as the Epomaker Glyph — and after testing this keyboard for a week, I don’t want to go back to the one I was previously using. The Glyph is a beast, sporting a charming typewriter-like design, while offering a wonderful typing experience and creamy sound.</p><p>With dual screens, pleasing RGB lighting, solid build quality, and user-friendly companion software to boot, the Glyph is easily one of the greatest keyboards you can buy right now. While it isn’t without its flaws — namely, limited stock switch choice and poor battery — as you’ll soon see, the Glyph’s upsides easily outweigh its shortcomings.</p><p>For the complete breakdown, read my full Epomaker Glyph review.</p><h2 class="article-body__section" id="section-epomaker-glyph-review-specs"><span>Epomaker Glyph 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/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">$139</a> / <a href="https://www.amazon.co.uk/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">£103</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Epomaker Wisteria V2 (linear)</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Top</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 83 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>8,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>13.66 x 8.14 x 2.24 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.59lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Retro White</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-epomaker-glyph-review-cheat-sheet"><span>Epomaker Glyph review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 75% typewriter-like mechanical keyboard with dual screens, and linear switches</li><li><strong>Who is it for? </strong>Those who type for a living and want a fun keyboard to do so</li><li><strong>How much does it cost? </strong>The Epomaker Glyph is available for <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">$139</a> / <a href="https://www.amazon.co.uk/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">£103</a></li><li><strong>What do we like? </strong>So much! The outstanding design, solid build quality, lovely typing experience, creamy sound profile, user-friendly companion software, and more</li><li><strong>What don’t we like? </strong>The poor battery life and limited stock switch set choice</li></ul><h2 class="article-body__section" id="section-epomaker-glyph-review-the-ups"><span>Epomaker Glyph review: The ups</span></h2><p>From its gorgeous typewriter-like design and solid build quality, to the awesome typing experience and creamy sound profile, the Epomaker Glyph is a winner.</p><h2 id="a-typewriter-like-design-done-right">A typewriter-like design done 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="sjAcau9VseSqLieN8EcHke" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/sjAcau9VseSqLieN8EcHke-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Glyph is Epomaker’s first venture into the world of typewriter-like keyboards. The only other typewriter-esque keyboard I’ve personally tested is the <a href="https://www.tomsguide.com/computing/peripherals/logitech-pop-keys-review">Logitech Pop Keys</a> ($99) — which the Glyph blows out of the water with ease — although we’ve also reviewed (and loved) the <a href="https://www.tomsguide.com/computing/keyboards/lofree-dot-review">Lofree DOT</a> ($99), which is a great deck if you want to save $40. </p><p>Featuring rounded keycaps and a poppy sound profile, the Glyph emulates the experience of typing on a typewriter, and I’ll discuss these in detail shortly. First, I want to talk about the keyboard’s design. Blending retro aesthetics with modern features, the Glyph adds a flair to your desk and is sure to draw eyeballs (in a good 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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Gb9f5kFzbbBRGcsBA72t7f" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Gb9f5kFzbbBRGcsBA72t7f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 south-facing RGB lighting doesn’t look loud or garish, and there’s an ambient light strip that lights up in the colors of the rainbow, with the light falling on the surface the keyboard is sitting on. I love the glossy finish on the retro white body, which feels soft and premium to touch (and look at).</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="CfTyBr5QuLHymePzaZYCGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CfTyBr5QuLHymePzaZYCGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Glyph sports a 75% layout with 83 keys, and it measures 13.66 x 8.14 x 2.24 inches so doesn’t take up too much space. It weighs 2.59lbs, so while it’s heavier than the likes of the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129, 1.67lbs) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139, 1.65lbs), I haven’t had any trouble carrying the Glyph in my backpack, to and from work.</p><p>Adding to the Glyph’s charm and typewriter-like design is a return lever located in the top-left corner, which as the Enter key when pushed up; and Backspace when pulled down. It feels a little gimmicky, I know, but it’s a harmless fun element, and I’ve been using it every so often while typing. In the top-right corner, you’ll find a rotary knob for adjusting the volume or swapping between display items (by holding down Fn).</p><h2 id="useful-dual-screens">Useful dual screens</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EYdkhYeZmr9GECguUTTAwf" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/EYdkhYeZmr9GECguUTTAwf-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Screens on keyboards are becoming more and more popular as we’ve seen on the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115) and the <a href="https://www.tomsguide.com/computing/peripherals/razer-blackwidow-v4-pro-75-percent-review">Razer BlackWidow V4</a> ($299). The Epomaker Glyph is no different.</p><p>Located above the keys are dual displays: one of which shows the date, month and year, and the other which shows the keyboards vitals, such as battery life, connection mode, active operating system, CPU usage, and more. You can also display a GIF or static image on the bigger display, if you like.</p><p>I like being able to quickly see the battery status, for instance, as it saves me from going into my laptop’s Bluetooth settings and wasting a couple of minutes.</p><h2 id="comfortable-wrist-rest-included">Comfortable wrist rest included</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7870px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nRE477cdhXmZMuKtHniAAN" name="Epomaker_Glyph" alt="Epomaker Glyph's wrist rest" src="https://cdn.mos.cms.futurecdn.net/nRE477cdhXmZMuKtHniAAN-1920-80.jpg" mos="" align="middle" fullscreen="" width="7870" height="4427" attribution="" endorsement="" class="inline"></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 something I don’t see with a lot of mid-range keyboards: the Epomaker Glyph comes with a wrist rest included in the box, and it’s wonderful. The CloudGel wrist rest is made of high-quality silicone and PVC materials, and it feels soft to rest my wrists on. It provides good elevation so my wrists sit flush against the keyboard’s length, and the gentle cushioning has elevated any pressure I would have felt otherwise — or without a wrist rest.</p><p>The Glyph’s wrist rest makes it ergonomically-friendly, as the slow-rebound gel deforms according to the pressure and shape of your wrists, so your hands feel comfortable. Having used the keyboard extensively for a week’s worth of typing, I can confirm that I haven’t felt any discomfort. The good news is that you can buy the wrist rest separately too, for <a href="https://epomaker.com/products/epomaker-cloudgel-wrist-rest" target="_blank" rel="nofollow">$24 / £18</a>, to pair with any other keyboard you own. I’m just happy that it doesn’t feel as hard as the <a href="https://www.tomsguide.com/computing/peripherals/corsair-k55-rgb-pro-review-just-good-enough">Corsair K55 RGB Pro</a>’s ($59).</p><h2 id="excellent-build-quality">Excellent build 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="qbniJkrVVRJTNn875qprGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/qbniJkrVVRJTNn875qprGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>A common issue I’ve faced with a few Epomaker keyboards is the shoddy build quality. Some of the brand’s all-plastic keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-cypher96-keyboard-review">Cypher96</a> ($89) and the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th99-review">TH99</a> ($82), have had some flex in their bodies, and their keys have dipped too far into the PCB (even for gasket mounted decks). The Glyph, on the other hand, boasts fantastic build quality, even though it’s made entirely of plastic.</p><p>I didn’t notice any flex in the Glyph’s body even when I tried to twist it by holding its sides, and the keys didn’t dip as deep into the PCB either, although this is primarily due to the Glyph using a top mounted PCB. Tapping the keyboard with my fingernails didn’t reveal any hollow-sounding parts. Rest assured that you’re getting bang for your buck here.</p><h2 id="just-so-much-fun-to-type-on">Just so much fun to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="74p25fJhGbVJscmLFXa28f" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/74p25fJhGbVJscmLFXa28f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 nearly 100 keyboards, and while many of them have been great to type on, some have been downright awful. The Epomaker Glyph falls into the former category, and it’s the greatest keyboard I’ve ever typed on. It comes fitted with pre-lubed Epomaker Wisteria V2 linear switches which require 38g of force to activate, and have a pre-travel distance of 2mm (total 3.6mm). This means that you don’t need to press them all the way down to activate them, so you can type quickly.</p><p>I love linear switches, and 38g of force is very light. I didn’t find the Wisteria V2s fatiguing to type on, and even after an eight-hour workday, my fingers didn’t feel tired at all. The switches sound lovely too, and I’ll talk about the Glyph’s sound profile shortly.</p><p>The Glyph features double-shot PBT keycaps which are more resistant to wear-and-tear and are oil-resistant, making them far more durable than the ABS keycaps used on the typewriter-like Logitech Pop Keys. They feel soft and comfortable and I feel like my fingers are gliding across the keyboard.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Epomaker Glyph</p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-kw-300-mx-review">Cherry KW 300 MX</a></p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.73</p></td><td  ><p>91.21%</p></td></tr></tbody></table></div><p>As I do with every other keyboard I test, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, and then compared the Glyph’s results to other mechanical decks I’ve reviewed. Straight out of the box, I achieved 109 words per minute with 93.2% accuracy — a new personal best. Seeing those stats on paper has made the Glyph a permanent fixture on my desk.</p><h2 id="seriously-addicting-sound">Seriously addicting sound</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="j878krmTCKq9w5caN8Hq4g" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/j878krmTCKq9w5caN8Hq4g-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Epomaker Glyph features a top-mount design which means that the switch plate is screwed directly to the case’s top half. Because each keystroke vibration travels directly into the case, this results in a louder, sharper sound profile. Given that the Glyph is a typewriter-like keyboard, this makes sense — and boy does the keyboard sound downright beautiful.</p><p>I’d best describe the Glyph’s sound profile as somewhere between creamy and clacky, and it feels like the perfect blend of the old and the new. The Glyph packs five layers of sound-dampening and noise-absorbing foams, and I didn’t notice any unnecessary or immersion-breaking metallic pings while typing. While it’s loud, it isn’t <em>too </em>loud and doesn’t disturb those around you — unlike the Logitech Pop Keys, which was replete with plasticky echoes and light metallic pings.</p><h2 id="user-friendly-companion-software-3">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/QoybM3PxwVpbiKASQQxfsM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/ApXHAyTV6m3Pya2HkJmxsM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/fMJZgXMoqg8kLRS2nyN8rM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/SxuDxqaP3UaB6fJjWQFMwM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/GcEwYLuCAXB2nuGWC6KVtM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure></figure><p>To customize the Epomaker Glyph, you’ll need to download <a href="https://epomaker.com/blogs/software/epomaker-driver-3-0" target="_blank" rel="nofollow">Epomaker Driver 3.0</a> which is available for free on macOS and Windows. It sports a clean interface with no intrusive pop-ups or ads, and I found it responsive and lag-free in my testing, having tested it on both operating systems.</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:400px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hMd6veZFaS8PiCtJt6wic6" name="Epomaker-Glyph" alt="A GIF of puffings displayed on an Epomaker Glyph keyboard" src="https://cdn.mos.cms.futurecdn.net/hMd6veZFaS8PiCtJt6wic6-1920-80.gif" mos="" align="middle" fullscreen="" width="400" height="225" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: National Geographic / Tom's Guide)</span></figcaption></figure><p>Via Epomaker Driver, you can remap the Glyph’s keys, record macros, adjust the RGB lighting’s colors and effects, and upload your own GIFs or static images and display them on the screen. You can update or flash the board’s firmware too. </p><p>I do, however, wish that Epomaker had its own web-based software, like Keychron does with Keychron Launcher and NuPhy does with NuPhy.io. That would save users downloading third-party software or bloatware on their machines, and would make customization easier. One can dream.</p><h2 class="article-body__section" id="section-epomaker-glyph-review-the-downs"><span>Epomaker Glyph review: The downs</span></h2><p>Though the Epomaker Glyph is an excellent keyboard, it isn’t without its shortcomings, and its limited stock switch choice and poor battery life are quite disappointing.</p><h2 id="very-limited-stock-switch-choice">Very limited stock switch choice</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BaY3N4p2Sup3v7bcrdva5g" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/BaY3N4p2Sup3v7bcrdva5g-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Limited stock switch choice seems to be a recurring theme (and issue) with Epomaker’s mechanical keyboards, and the Glyph is no different. Just like the TH99, <a href="https://www.tomsguide.com/computing/keyboards/epomaker-p65-review">P65</a> ($119) and Cypher96, to name a few, there aren’t many stock switch options available. In fact, there’s just <em>one </em>available, and that’s the Wisteria V2 linears that I naturally tested. The switches are no doubt fantastic, but those who prefer tactile or clicky ones may be left feeling shortchanged.</p><p>The good news is that the Glyph is hot-swappable, and it’s compatible with 3- and 5-pin south-facing mechanical switches. If you have extra switches lying around, or you simply buy some, you can replace them without de- and re-soldering them to the PCB.</p><p>Of course, you may want more switch choices at the time of purchase, and if that’s the case, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129), the <a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-pro-review">Lemokey P1 Pro </a>($129) or the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). None look as unique as the Glyph, though.</p><h2 id="screen-and-rgb-eat-into-the-battery">Screen and RGB eat into the 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="DBydbu7kndB3kRQBCmoeBf" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/DBydbu7kndB3kRQBCmoeBf-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 you can probably glean from what I said earlier, I love the Epomaker Glyph’s screens and RGB lighting — but they <em>really </em>eat into the battery. The Glyph is fitted with an 8,000mAh rechargeable battery which should last you a month or so, depending on RGB and screen settings. I’ve been using the Glyph with these settings turned all the way up, and I’ve already had to plug it in for charging once (after about 20 hours of use).</p><p>I’m not saying this is entirely the keyboard’s fault. Many decks with extensive lighting systems and screens don’t last nearly as long. For instance, I was able to get only 20 hours of runtime out of the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> ($259) — nine hours fewer than Corsair’s claim. I sorely wish the Glyph lasted longer because the dual screens are delightful, and I don’t want to turn them off.</p><h2 class="article-body__section" id="section-epomaker-glyph-review-verdict"><span>Epomaker Glyph 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="Z3zvU2RMYGW2bjMs6Bubwe" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Z3zvU2RMYGW2bjMs6Bubwe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 can’t remember the last time I had so much fun testing a keyboard. The Epomaker Glyph is one of the best mechanical decks you can buy right now. Featuring responsive and light linear switches that are a joy to type on, and offering a creamy sound profile, the Glyph is perfect for those who type for a living.</p><p>The Glyph is chock-full of goodies. Its typewriter-like design is great to look at, and its dual screen setup as well as gorgeous RGB lighting add a colorful flair to your setup. User-friendly companion software makes customization a walk in the park, too. While it would have been nice to have more than one set of stock switches and better battery life, neither of those are dealbreakers, all things considered.</p><p>At the end of the day, the Glyph is the ultimate keyboard for typists. It’s one of the best gadgets I’ve reviewed so far, and it puts the fun back into typing.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/epomaker-glyph-review</link>
                                                                            <description>
                            <![CDATA[ The Epomaker Glyph is a wonderful keyboard to type on, thanks to its responsive linear switches, typewriter-like design and sound profile, and dual screens. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">oY9gRFatEoDTX6mjHJwbZQ</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/N2DCJWYTMHyPrsofABJbtf-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 11 Jun 2026 16:44:15 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/N2DCJWYTMHyPrsofABJbtf-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[An Epomaker Glyph mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[An Epomaker Glyph mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[An Epomaker Glyph mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/N2DCJWYTMHyPrsofABJbtf-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>As someone whose bread and butter revolves around typing, I need one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> to get the job done. Mech decks can be fun, but few are as fun as the Epomaker Glyph — and after testing this keyboard for a week, I don’t want to go back to the one I was previously using. The Glyph is a beast, sporting a charming typewriter-like design, while offering a wonderful typing experience and creamy sound.</p><p>With dual screens, pleasing RGB lighting, solid build quality, and user-friendly companion software to boot, the Glyph is easily one of the greatest keyboards you can buy right now. While it isn’t without its flaws — namely, limited stock switch choice and poor battery — as you’ll soon see, the Glyph’s upsides easily outweigh its shortcomings.</p><p>For the complete breakdown, read my full Epomaker Glyph review.</p><h2 class="article-body__section" id="section-epomaker-glyph-review-specs"><span>Epomaker Glyph 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/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">$139</a> / <a href="https://www.amazon.co.uk/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">£103</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Epomaker Wisteria V2 (linear)</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Top</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 83 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>8,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>13.66 x 8.14 x 2.24 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.59lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Retro White</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-epomaker-glyph-review-cheat-sheet"><span>Epomaker Glyph review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A 75% typewriter-like mechanical keyboard with dual screens, and linear switches</li><li><strong>Who is it for? </strong>Those who type for a living and want a fun keyboard to do so</li><li><strong>How much does it cost? </strong>The Epomaker Glyph is available for <a href="https://www.amazon.com/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">$139</a> / <a href="https://www.amazon.co.uk/EPOMAKER-Glyph-Typewriter-Mechanical-Top-Mount/dp/B0GJ4V9XFJ/" target="_blank" rel="nofollow">£103</a></li><li><strong>What do we like? </strong>So much! The outstanding design, solid build quality, lovely typing experience, creamy sound profile, user-friendly companion software, and more</li><li><strong>What don’t we like? </strong>The poor battery life and limited stock switch set choice</li></ul><h2 class="article-body__section" id="section-epomaker-glyph-review-the-ups"><span>Epomaker Glyph review: The ups</span></h2><p>From its gorgeous typewriter-like design and solid build quality, to the awesome typing experience and creamy sound profile, the Epomaker Glyph is a winner.</p><h2 id="a-typewriter-like-design-done-right">A typewriter-like design done 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="sjAcau9VseSqLieN8EcHke" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/sjAcau9VseSqLieN8EcHke-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Glyph is Epomaker’s first venture into the world of typewriter-like keyboards. The only other typewriter-esque keyboard I’ve personally tested is the <a href="https://www.tomsguide.com/computing/peripherals/logitech-pop-keys-review">Logitech Pop Keys</a> ($99) — which the Glyph blows out of the water with ease — although we’ve also reviewed (and loved) the <a href="https://www.tomsguide.com/computing/keyboards/lofree-dot-review">Lofree DOT</a> ($99), which is a great deck if you want to save $40. </p><p>Featuring rounded keycaps and a poppy sound profile, the Glyph emulates the experience of typing on a typewriter, and I’ll discuss these in detail shortly. First, I want to talk about the keyboard’s design. Blending retro aesthetics with modern features, the Glyph adds a flair to your desk and is sure to draw eyeballs (in a good 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:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Gb9f5kFzbbBRGcsBA72t7f" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Gb9f5kFzbbBRGcsBA72t7f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 south-facing RGB lighting doesn’t look loud or garish, and there’s an ambient light strip that lights up in the colors of the rainbow, with the light falling on the surface the keyboard is sitting on. I love the glossy finish on the retro white body, which feels soft and premium to touch (and look at).</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="CfTyBr5QuLHymePzaZYCGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CfTyBr5QuLHymePzaZYCGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Glyph sports a 75% layout with 83 keys, and it measures 13.66 x 8.14 x 2.24 inches so doesn’t take up too much space. It weighs 2.59lbs, so while it’s heavier than the likes of the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129, 1.67lbs) and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139, 1.65lbs), I haven’t had any trouble carrying the Glyph in my backpack, to and from work.</p><p>Adding to the Glyph’s charm and typewriter-like design is a return lever located in the top-left corner, which as the Enter key when pushed up; and Backspace when pulled down. It feels a little gimmicky, I know, but it’s a harmless fun element, and I’ve been using it every so often while typing. In the top-right corner, you’ll find a rotary knob for adjusting the volume or swapping between display items (by holding down Fn).</p><h2 id="useful-dual-screens">Useful dual screens</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EYdkhYeZmr9GECguUTTAwf" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/EYdkhYeZmr9GECguUTTAwf-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Screens on keyboards are becoming more and more popular as we’ve seen on the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115) and the <a href="https://www.tomsguide.com/computing/peripherals/razer-blackwidow-v4-pro-75-percent-review">Razer BlackWidow V4</a> ($299). The Epomaker Glyph is no different.</p><p>Located above the keys are dual displays: one of which shows the date, month and year, and the other which shows the keyboards vitals, such as battery life, connection mode, active operating system, CPU usage, and more. You can also display a GIF or static image on the bigger display, if you like.</p><p>I like being able to quickly see the battery status, for instance, as it saves me from going into my laptop’s Bluetooth settings and wasting a couple of minutes.</p><h2 id="comfortable-wrist-rest-included">Comfortable wrist rest included</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:7870px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nRE477cdhXmZMuKtHniAAN" name="Epomaker_Glyph" alt="Epomaker Glyph's wrist rest" src="https://cdn.mos.cms.futurecdn.net/nRE477cdhXmZMuKtHniAAN-1920-80.jpg" mos="" align="middle" fullscreen="" width="7870" height="4427" attribution="" endorsement="" class="inline"></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 something I don’t see with a lot of mid-range keyboards: the Epomaker Glyph comes with a wrist rest included in the box, and it’s wonderful. The CloudGel wrist rest is made of high-quality silicone and PVC materials, and it feels soft to rest my wrists on. It provides good elevation so my wrists sit flush against the keyboard’s length, and the gentle cushioning has elevated any pressure I would have felt otherwise — or without a wrist rest.</p><p>The Glyph’s wrist rest makes it ergonomically-friendly, as the slow-rebound gel deforms according to the pressure and shape of your wrists, so your hands feel comfortable. Having used the keyboard extensively for a week’s worth of typing, I can confirm that I haven’t felt any discomfort. The good news is that you can buy the wrist rest separately too, for <a href="https://epomaker.com/products/epomaker-cloudgel-wrist-rest" target="_blank" rel="nofollow">$24 / £18</a>, to pair with any other keyboard you own. I’m just happy that it doesn’t feel as hard as the <a href="https://www.tomsguide.com/computing/peripherals/corsair-k55-rgb-pro-review-just-good-enough">Corsair K55 RGB Pro</a>’s ($59).</p><h2 id="excellent-build-quality">Excellent build 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="qbniJkrVVRJTNn875qprGg" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/qbniJkrVVRJTNn875qprGg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>A common issue I’ve faced with a few Epomaker keyboards is the shoddy build quality. Some of the brand’s all-plastic keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-cypher96-keyboard-review">Cypher96</a> ($89) and the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th99-review">TH99</a> ($82), have had some flex in their bodies, and their keys have dipped too far into the PCB (even for gasket mounted decks). The Glyph, on the other hand, boasts fantastic build quality, even though it’s made entirely of plastic.</p><p>I didn’t notice any flex in the Glyph’s body even when I tried to twist it by holding its sides, and the keys didn’t dip as deep into the PCB either, although this is primarily due to the Glyph using a top mounted PCB. Tapping the keyboard with my fingernails didn’t reveal any hollow-sounding parts. Rest assured that you’re getting bang for your buck here.</p><h2 id="just-so-much-fun-to-type-on">Just so much fun to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="74p25fJhGbVJscmLFXa28f" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/74p25fJhGbVJscmLFXa28f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 nearly 100 keyboards, and while many of them have been great to type on, some have been downright awful. The Epomaker Glyph falls into the former category, and it’s the greatest keyboard I’ve ever typed on. It comes fitted with pre-lubed Epomaker Wisteria V2 linear switches which require 38g of force to activate, and have a pre-travel distance of 2mm (total 3.6mm). This means that you don’t need to press them all the way down to activate them, so you can type quickly.</p><p>I love linear switches, and 38g of force is very light. I didn’t find the Wisteria V2s fatiguing to type on, and even after an eight-hour workday, my fingers didn’t feel tired at all. The switches sound lovely too, and I’ll talk about the Glyph’s sound profile shortly.</p><p>The Glyph features double-shot PBT keycaps which are more resistant to wear-and-tear and are oil-resistant, making them far more durable than the ABS keycaps used on the typewriter-like Logitech Pop Keys. They feel soft and comfortable and I feel like my fingers are gliding across the keyboard.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Epomaker Glyph</p></td><td  ><p>109</p></td><td  ><p>93.2%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-kw-300-mx-review">Cherry KW 300 MX</a></p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.73</p></td><td  ><p>91.21%</p></td></tr></tbody></table></div><p>As I do with every other keyboard I test, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, and then compared the Glyph’s results to other mechanical decks I’ve reviewed. Straight out of the box, I achieved 109 words per minute with 93.2% accuracy — a new personal best. Seeing those stats on paper has made the Glyph a permanent fixture on my desk.</p><h2 id="seriously-addicting-sound">Seriously addicting sound</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="j878krmTCKq9w5caN8Hq4g" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/j878krmTCKq9w5caN8Hq4g-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Epomaker Glyph features a top-mount design which means that the switch plate is screwed directly to the case’s top half. Because each keystroke vibration travels directly into the case, this results in a louder, sharper sound profile. Given that the Glyph is a typewriter-like keyboard, this makes sense — and boy does the keyboard sound downright beautiful.</p><p>I’d best describe the Glyph’s sound profile as somewhere between creamy and clacky, and it feels like the perfect blend of the old and the new. The Glyph packs five layers of sound-dampening and noise-absorbing foams, and I didn’t notice any unnecessary or immersion-breaking metallic pings while typing. While it’s loud, it isn’t <em>too </em>loud and doesn’t disturb those around you — unlike the Logitech Pop Keys, which was replete with plasticky echoes and light metallic pings.</p><h2 id="user-friendly-companion-software-3">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/QoybM3PxwVpbiKASQQxfsM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/ApXHAyTV6m3Pya2HkJmxsM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/fMJZgXMoqg8kLRS2nyN8rM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/SxuDxqaP3UaB6fJjWQFMwM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/GcEwYLuCAXB2nuGWC6KVtM-1920-80.jpg" alt="Epomaker Driver screenshots" /><figcaption><small role="credit">Epomaker</small></figcaption></figure></figure><p>To customize the Epomaker Glyph, you’ll need to download <a href="https://epomaker.com/blogs/software/epomaker-driver-3-0" target="_blank" rel="nofollow">Epomaker Driver 3.0</a> which is available for free on macOS and Windows. It sports a clean interface with no intrusive pop-ups or ads, and I found it responsive and lag-free in my testing, having tested it on both operating systems.</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:400px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="hMd6veZFaS8PiCtJt6wic6" name="Epomaker-Glyph" alt="A GIF of puffings displayed on an Epomaker Glyph keyboard" src="https://cdn.mos.cms.futurecdn.net/hMd6veZFaS8PiCtJt6wic6-1920-80.gif" mos="" align="middle" fullscreen="" width="400" height="225" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: National Geographic / Tom's Guide)</span></figcaption></figure><p>Via Epomaker Driver, you can remap the Glyph’s keys, record macros, adjust the RGB lighting’s colors and effects, and upload your own GIFs or static images and display them on the screen. You can update or flash the board’s firmware too. </p><p>I do, however, wish that Epomaker had its own web-based software, like Keychron does with Keychron Launcher and NuPhy does with NuPhy.io. That would save users downloading third-party software or bloatware on their machines, and would make customization easier. One can dream.</p><h2 class="article-body__section" id="section-epomaker-glyph-review-the-downs"><span>Epomaker Glyph review: The downs</span></h2><p>Though the Epomaker Glyph is an excellent keyboard, it isn’t without its shortcomings, and its limited stock switch choice and poor battery life are quite disappointing.</p><h2 id="very-limited-stock-switch-choice">Very limited stock switch choice</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="BaY3N4p2Sup3v7bcrdva5g" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/BaY3N4p2Sup3v7bcrdva5g-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Limited stock switch choice seems to be a recurring theme (and issue) with Epomaker’s mechanical keyboards, and the Glyph is no different. Just like the TH99, <a href="https://www.tomsguide.com/computing/keyboards/epomaker-p65-review">P65</a> ($119) and Cypher96, to name a few, there aren’t many stock switch options available. In fact, there’s just <em>one </em>available, and that’s the Wisteria V2 linears that I naturally tested. The switches are no doubt fantastic, but those who prefer tactile or clicky ones may be left feeling shortchanged.</p><p>The good news is that the Glyph is hot-swappable, and it’s compatible with 3- and 5-pin south-facing mechanical switches. If you have extra switches lying around, or you simply buy some, you can replace them without de- and re-soldering them to the PCB.</p><p>Of course, you may want more switch choices at the time of purchase, and if that’s the case, I’d recommend the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129), the <a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-pro-review">Lemokey P1 Pro </a>($129) or the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). None look as unique as the Glyph, though.</p><h2 id="screen-and-rgb-eat-into-the-battery">Screen and RGB eat into the 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="DBydbu7kndB3kRQBCmoeBf" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/DBydbu7kndB3kRQBCmoeBf-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 you can probably glean from what I said earlier, I love the Epomaker Glyph’s screens and RGB lighting — but they <em>really </em>eat into the battery. The Glyph is fitted with an 8,000mAh rechargeable battery which should last you a month or so, depending on RGB and screen settings. I’ve been using the Glyph with these settings turned all the way up, and I’ve already had to plug it in for charging once (after about 20 hours of use).</p><p>I’m not saying this is entirely the keyboard’s fault. Many decks with extensive lighting systems and screens don’t last nearly as long. For instance, I was able to get only 20 hours of runtime out of the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> ($259) — nine hours fewer than Corsair’s claim. I sorely wish the Glyph lasted longer because the dual screens are delightful, and I don’t want to turn them off.</p><h2 class="article-body__section" id="section-epomaker-glyph-review-verdict"><span>Epomaker Glyph 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="Z3zvU2RMYGW2bjMs6Bubwe" name="Epomaker_Glyph" alt="An Epomaker Glyph mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Z3zvU2RMYGW2bjMs6Bubwe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 can’t remember the last time I had so much fun testing a keyboard. The Epomaker Glyph is one of the best mechanical decks you can buy right now. Featuring responsive and light linear switches that are a joy to type on, and offering a creamy sound profile, the Glyph is perfect for those who type for a living.</p><p>The Glyph is chock-full of goodies. Its typewriter-like design is great to look at, and its dual screen setup as well as gorgeous RGB lighting add a colorful flair to your setup. User-friendly companion software makes customization a walk in the park, too. While it would have been nice to have more than one set of stock switches and better battery life, neither of those are dealbreakers, all things considered.</p><p>At the end of the day, the Glyph is the ultimate keyboard for typists. It’s one of the best gadgets I’ve reviewed so far, and it puts the fun back into typing.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Gimmick or genius? I was skeptical about the Logitech M850L’s built-in cushioned palm rest — then I tested it ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Mice aren't particularly exciting. Some might even argue that these rodents that help us do our work or even excel at gaming are boring. You aren't entirely wrong in thinking that. I've tested my fair share of the <a href="https://www.tomsguide.com/best-picks/best-mouse">best mice</a>, and more often than not, they look the same or have the same or similar features. There isn't much that's exciting about rodents.</p><p>But what if I told you that I've just tested a mouse that <em>actually </em>does something different? That low-key <em>revolutionizes </em>the world of productivity mice? "That's not possible," I hear you mumble under your breath, but it's true. The <a href="https://www.tomsguide.com/computing/mice/logitech-signature-comfort-plus-m850l-review">Logitech Signature Comfort Plus M850L</a> is unlike any other mouse I've tested, and after having used it for a week, I can't go back to traditional mice.</p><div class="product"><a data-dimension112="08f4bf13-721d-4ecf-8e5d-45a2dc1c2271" data-action="Deal Block" data-label="Signature Comfort Plus M850L" data-dimension48="Signature Comfort Plus M850L" data-dimension25="$49" href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:868px;"><p class="vanilla-image-block" style="padding-top:164.06%;"><img id="bhPKtkTtJv74vxwiYP45HN" name="M850L-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/bhPKtkTtJv74vxwiYP45HN-1920-80.jpg" mos="" align="middle" fullscreen="" width="868" height="1424" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Logitech Signature Comfort Plus M850L is one of the best productivity mice today, period. For office workers on a budget, the M850L delivers unparalleled comfort for just $49. Thanks to its built-in cushioned palm rest, contoured shape and 24 months of battery life, it’s meant for all-day work.<a class="view-deal button" href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow" data-dimension112="08f4bf13-721d-4ecf-8e5d-45a2dc1c2271" data-action="Deal Block" data-label="Signature Comfort Plus M850L" data-dimension48="Signature Comfort Plus M850L" data-dimension25="$49">View Deal</a></p></div><p>So, what makes the M850L unique? It's got a built-in cushioned palm rest, for starters, and that alone makes it worth the asking price — and it isn't too much, either. The M850L is available for just $49. Forty. Nine. Dollars. And for that money, it feels like the endgame of budget productivity mice. But is the palm rest a gimmick or a stroke of genius? I'm here to answer that question (spoiler alert: it's the latter).</p><h2 id="it-ain-t-vertical-or-a-trackball-but-it-s-still-super-ergonomic">It ain't vertical or a trackball, but it's still super ergonomic</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nPEaJQRaeE7kkYMZcUbLq3" name="Logi_M850L_Mouse_014.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/nPEaJQRaeE7kkYMZcUbLq3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 we talk about ergonomic mice, trackball and vertical rodents spring to mind. The <a href="https://www.tomsguide.com/computing/peripherals/lenovo-go-wireless-vertical-review">Lenovo Go Wireless Vertical</a> and the <a href="https://www.tomsguide.com/computing/peripherals/logitech-ergo-m575-review">Logitech Ergo M575</a> are great examples of ergonomics done right. The Logitech Signature Comfort Plus M850L doesn't feature a trackball, and it isn't vertical, and yet, it's extremely comfortable. That's thanks to its shape and the aforementioned cushioned palm rest.</p><p>The M850L sports a contoured shape with rubber grips running along its length. These rubber grips are soft, so your thumb feels comfortable. The contoured shape also enables your hand to sit more naturally on the mouse. Then you've got the palm rest located behind the left and right click buttons. It's made of durable and squishy material and it's designed to provide some ergonomic relief by reducing pressure on the main part of your palm.</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:758px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="6QkwX9Bo2VspAVmNnzhqL4" name="Logi_M850L_Mouse_004.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:139,l:426,cw:758,ch:426,q:80/6QkwX9Bo2VspAVmNnzhqL4.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though I was skeptical when I attended the press briefing, it took just under an hour for me to come around when I started testing the M850L. If you were given a choice between a soft, down feather pillow and a hard rocky surface, you'd choose the former to rest your head on, right? That's what the M850L's palm rest feels like, and I'd choose it over a traditional mouse any day.</p><h2 id="nothing-short-of-a-productivity-beast">Nothing short of a productivity beast</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wUFEq8uqNZdeowAzUnM4t3" name="Logi_M850L_Mouse_013.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/wUFEq8uqNZdeowAzUnM4t3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Alongside delivering all-day comfort, the Logitech Signature Comfort Plus M850L is a beast for productivity. If you need a mouse that will enable you to work for hours on end without disturbing your co-workers, and you want to maximize your productivity, this is the one to get.</p><p>The M850L is a speedy pointer — as productivity mice go, at least. You can swap between two customizable levels of DPI (up to 4,000) depending on the task you're doing. Its left- and right-click buttons are fitted with silent microswitches designed to reduce noise by up to 90%, so you don't need to worry about disturbing your co-workers.</p><p>What I love about the M850L is that Logitech has brought over a vaunted feature from its pricier work-first mice, and that's the SmartWheel. Powered by this technology, the M850L's scroll wheel automatically switches between tactile (ratchet) and super-fast (free-spin) modes based on how fast you're spinning it. This is a game-changer for productivity, as you don't need to waste seconds on manually changing the mode.</p><h2 id="best-part-it-doesn-t-break-the-bank">Best part? It doesn't break the bank</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zkJZyACPFUyeHzrzMsf5j3" name="Logi_M850L_Mouse_011.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/zkJZyACPFUyeHzrzMsf5j3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 just how excellent the mouse is, you might be thinking the Logitech Signature Comfort Plus M850L is pricey. That couldn't be further from the truth. The M850L is one of the cheapest mice I've ever tested, and you can buy it in Off-White, Graphite or Black for just <a href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49 at Amazon</a>.</p><p>To give you a point of reference, here in the U.K. where I'm based, a cappuccino costs £4, so you can get about 10 coffees for the same price as the M850L. Given the joy coffee brings some people — including myself — I'm confident that the M850L will bring you just as much happiness if not more, especially if you're a sedentary worker.</p><p>As someone who spends up to eight hours a day, five days a week, at a desk, I can't overstate the importance of an ergonomic mouse. The M850L is a competent and comfortable mouse, and I love its cushioned palm rest so much that I now wish every productivity-oriented mouse had one. And it's earned the M850L a permanent spot on my desk.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eER3kW"></div>                            </div>                            <script src="https://kwizly.com/embed/eER3kW.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/computing/mice/logitech-signature-comfort-plus-m850l-review">‘The endgame of budget productivity mice’: I tested the $49 Logitech M850L and its built-in cushioned palm rest delivers unparalleled comfort</a></li><li><a href="https://www.tomsguide.com/best-picks/best-wireless-mouse">The best wireless mouse in 2026 — tested and reviewed</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/do-not-buy-that-8-000hz-polling-rate-gaming-mouse-im-not-wasting-my-money-again">Do not buy that 8,000Hz polling rate gaming mouse — I'm not wasting my money again</a></li></ul><div data-widget-type="review" data-model-name="Logitech Signature Comfort Plus M850L"></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/mice/gimmick-or-genius-i-was-skeptical-about-the-logitech-m850ls-built-in-cushioned-palm-rest-then-i-tested-it</link>
                                                                            <description>
                            <![CDATA[ The Logitech M850L is the brand's first mouse with a built-in cushioned palm rest and though it sounds gimmicky, it's actually useful. Here's how. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">n996DW3nmAGc9FtJDwEcaK</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/uhPKSpvjkPP9NS8NrrWH74-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 06 Jun 2026 06:00:00 +0000</pubDate>                                                                                                                                <updated>Wed, 10 Jun 2026 13:55:00 +0000</updated>
                                                                                                                                            <category><![CDATA[Mice]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/uhPKSpvjkPP9NS8NrrWH74-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Logitech Signature Comfort Plus M850L mouse]]></media:description>                                                            <media:text><![CDATA[A Logitech Signature Comfort Plus M850L mouse]]></media:text>
                                <media:title type="plain"><![CDATA[A Logitech Signature Comfort Plus M850L mouse]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/uhPKSpvjkPP9NS8NrrWH74-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Mice aren't particularly exciting. Some might even argue that these rodents that help us do our work or even excel at gaming are boring. You aren't entirely wrong in thinking that. I've tested my fair share of the <a href="https://www.tomsguide.com/best-picks/best-mouse">best mice</a>, and more often than not, they look the same or have the same or similar features. There isn't much that's exciting about rodents.</p><p>But what if I told you that I've just tested a mouse that <em>actually </em>does something different? That low-key <em>revolutionizes </em>the world of productivity mice? "That's not possible," I hear you mumble under your breath, but it's true. The <a href="https://www.tomsguide.com/computing/mice/logitech-signature-comfort-plus-m850l-review">Logitech Signature Comfort Plus M850L</a> is unlike any other mouse I've tested, and after having used it for a week, I can't go back to traditional mice.</p><div class="product"><a data-dimension112="08f4bf13-721d-4ecf-8e5d-45a2dc1c2271" data-action="Deal Block" data-label="Signature Comfort Plus M850L" data-dimension48="Signature Comfort Plus M850L" data-dimension25="$49" href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:868px;"><p class="vanilla-image-block" style="padding-top:164.06%;"><img id="bhPKtkTtJv74vxwiYP45HN" name="M850L-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/bhPKtkTtJv74vxwiYP45HN-1920-80.jpg" mos="" align="middle" fullscreen="" width="868" height="1424" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Logitech Signature Comfort Plus M850L is one of the best productivity mice today, period. For office workers on a budget, the M850L delivers unparalleled comfort for just $49. Thanks to its built-in cushioned palm rest, contoured shape and 24 months of battery life, it’s meant for all-day work.<a class="view-deal button" href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow" data-dimension112="08f4bf13-721d-4ecf-8e5d-45a2dc1c2271" data-action="Deal Block" data-label="Signature Comfort Plus M850L" data-dimension48="Signature Comfort Plus M850L" data-dimension25="$49">View Deal</a></p></div><p>So, what makes the M850L unique? It's got a built-in cushioned palm rest, for starters, and that alone makes it worth the asking price — and it isn't too much, either. The M850L is available for just $49. Forty. Nine. Dollars. And for that money, it feels like the endgame of budget productivity mice. But is the palm rest a gimmick or a stroke of genius? I'm here to answer that question (spoiler alert: it's the latter).</p><h2 id="it-ain-t-vertical-or-a-trackball-but-it-s-still-super-ergonomic">It ain't vertical or a trackball, but it's still super ergonomic</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nPEaJQRaeE7kkYMZcUbLq3" name="Logi_M850L_Mouse_014.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/nPEaJQRaeE7kkYMZcUbLq3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 we talk about ergonomic mice, trackball and vertical rodents spring to mind. The <a href="https://www.tomsguide.com/computing/peripherals/lenovo-go-wireless-vertical-review">Lenovo Go Wireless Vertical</a> and the <a href="https://www.tomsguide.com/computing/peripherals/logitech-ergo-m575-review">Logitech Ergo M575</a> are great examples of ergonomics done right. The Logitech Signature Comfort Plus M850L doesn't feature a trackball, and it isn't vertical, and yet, it's extremely comfortable. That's thanks to its shape and the aforementioned cushioned palm rest.</p><p>The M850L sports a contoured shape with rubber grips running along its length. These rubber grips are soft, so your thumb feels comfortable. The contoured shape also enables your hand to sit more naturally on the mouse. Then you've got the palm rest located behind the left and right click buttons. It's made of durable and squishy material and it's designed to provide some ergonomic relief by reducing pressure on the main part of your palm.</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:758px;"><p class="vanilla-image-block" style="padding-top:56.20%;"><img id="6QkwX9Bo2VspAVmNnzhqL4" name="Logi_M850L_Mouse_004.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:139,l:426,cw:758,ch:426,q:80/6QkwX9Bo2VspAVmNnzhqL4.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Though I was skeptical when I attended the press briefing, it took just under an hour for me to come around when I started testing the M850L. If you were given a choice between a soft, down feather pillow and a hard rocky surface, you'd choose the former to rest your head on, right? That's what the M850L's palm rest feels like, and I'd choose it over a traditional mouse any day.</p><h2 id="nothing-short-of-a-productivity-beast">Nothing short of a productivity beast</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wUFEq8uqNZdeowAzUnM4t3" name="Logi_M850L_Mouse_013.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/wUFEq8uqNZdeowAzUnM4t3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Alongside delivering all-day comfort, the Logitech Signature Comfort Plus M850L is a beast for productivity. If you need a mouse that will enable you to work for hours on end without disturbing your co-workers, and you want to maximize your productivity, this is the one to get.</p><p>The M850L is a speedy pointer — as productivity mice go, at least. You can swap between two customizable levels of DPI (up to 4,000) depending on the task you're doing. Its left- and right-click buttons are fitted with silent microswitches designed to reduce noise by up to 90%, so you don't need to worry about disturbing your co-workers.</p><p>What I love about the M850L is that Logitech has brought over a vaunted feature from its pricier work-first mice, and that's the SmartWheel. Powered by this technology, the M850L's scroll wheel automatically switches between tactile (ratchet) and super-fast (free-spin) modes based on how fast you're spinning it. This is a game-changer for productivity, as you don't need to waste seconds on manually changing the mode.</p><h2 id="best-part-it-doesn-t-break-the-bank">Best part? It doesn't break the bank</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zkJZyACPFUyeHzrzMsf5j3" name="Logi_M850L_Mouse_011.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/zkJZyACPFUyeHzrzMsf5j3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 just how excellent the mouse is, you might be thinking the Logitech Signature Comfort Plus M850L is pricey. That couldn't be further from the truth. The M850L is one of the cheapest mice I've ever tested, and you can buy it in Off-White, Graphite or Black for just <a href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49 at Amazon</a>.</p><p>To give you a point of reference, here in the U.K. where I'm based, a cappuccino costs £4, so you can get about 10 coffees for the same price as the M850L. Given the joy coffee brings some people — including myself — I'm confident that the M850L will bring you just as much happiness if not more, especially if you're a sedentary worker.</p><p>As someone who spends up to eight hours a day, five days a week, at a desk, I can't overstate the importance of an ergonomic mouse. The M850L is a competent and comfortable mouse, and I love its cushioned palm rest so much that I now wish every productivity-oriented mouse had one. And it's earned the M850L a permanent spot on my desk.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-eER3kW"></div>                            </div>                            <script src="https://kwizly.com/embed/eER3kW.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/computing/mice/logitech-signature-comfort-plus-m850l-review">‘The endgame of budget productivity mice’: I tested the $49 Logitech M850L and its built-in cushioned palm rest delivers unparalleled comfort</a></li><li><a href="https://www.tomsguide.com/best-picks/best-wireless-mouse">The best wireless mouse in 2026 — tested and reviewed</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/do-not-buy-that-8-000hz-polling-rate-gaming-mouse-im-not-wasting-my-money-again">Do not buy that 8,000Hz polling rate gaming mouse — I'm not wasting my money again</a></li></ul><div data-widget-type="review" data-model-name="Logitech Signature Comfort Plus M850L"></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ ‘The endgame of budget productivity mice’: I tested the $49 Logitech M850L and its built-in cushioned palm rest delivers unparalleled comfort ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Few manufacturers are better than Logitech when it comes to creating comfortable productivity gadgets, and the Swiss-American giant has done it once again. The Logitech Signature Comfort Plus M850L is unlike any other Logi mouse as it features a built-in cushioned palm rest that delivers all-day comfort.</p><p>The M850L is a beast for productivity, thanks to its speed-adaptive smart scroll wheel, quiet clicks, and extensive customization capabilities. But it isn’t perfect — few things in life are. The mouse uses disposable batteries and doesn’t come with a 2.4GHz receiver. Are either of those dealbreakers? Not at all, given that the mouse costs just $49.</p><p>To find out if this is the <a href="https://www.tomsguide.com/best-picks/best-mouse">best mouse</a> for you, read my full Logitech Signature Comfort Plus M850L review.</p><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-specs"><span>Logitech Signature Comfort Plus M850L 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/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>4,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>6</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.79 x 2.92 x 1.74 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>3.8oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Graphite, Off-White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, Logi Bolt USB Receiver</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>1 x AA</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>24 months</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-cheat-sheet"><span>Logitech Signature Comfort Plus M850L review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A right-handed rodent and Logitech’s first mouse with an integrated palm cushion</li><li><strong>Who is it for?</strong> For sedentary workers needing a comfortable mouse</li><li><strong>How much does it cost? </strong>The Logitech Signature Comfort Plus M850L is available for just <a href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49</a></li><li><strong>What do we like?</strong> Its built-in cushioned palm rest, overall comfort, user-friendly companion software, quiet clicks, and speed-adaptive scroll wheel</li><li><strong>What don’t we like? </strong>It uses disposable batteries and doesn’t come with a 2.4GHz receiver</li></ul><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-the-ups"><span>Logitech Signature Comfort Plus M850L review: The ups</span></h2><p>From its eco-friendly design, built-in cushioned palm rest, quiet clicks and speed-adaptive scroll wheel, the Logitech Signature Comfort Plus M850L is perhaps the best budget productivity mouse you can buy today.</p><h2 id="low-key-and-eco-friendly-design">Low-key and eco-friendly 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="xptrj3v79yLMf4RUbctVj3" name="Logi_M850L_Mouse_003.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/xptrj3v79yLMf4RUbctVj3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Signature Comfort Plus M850L is a productivity-oriented mouse for the office, and it’s designed as such. It sports a minimalist, low-key design that doesn’t draw eyeballs to your desk thanks to the lack of any flashy RGB or lighting. The mouse is available in three colors: Graphite, Black, and Off-White. I tested the Graphite model which is covered in a couple shades of light and dark gray, and it looks fantastic.</p><p>Measuring 4.79 x 2.92 x 1.74 inches, the M850L is ideal for those with small- or medium-sized hands. It weighs 3.8 ounces so it’s got a bit of heft to it, which is nice — it makes the mouse feel sturdy and substantial when your hand is cupping it. The textured surface that runs along the length of the body gives your thumb a good grip over the mouse. I’ll discuss the overall comfort in the following section.</p><p>The M850L feels extremely well-built, and you can tell that the plastic it utilizes is of high quality. I also like that the Graphite and Black models are made of 63% recycled material (49% on the Off-White version), and the mouse comes in paper packaging derived from <a href="https://uk.fsc.org/what-is-fsc/fsc-certified-forests" target="_blank" rel="nofollow">FSC-certified forests</a>.</p><h2 id="all-day-comfort">All-day comfort</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="K7DXe5W4bthmwzvE4ikgx3" name="Logi_M850L_Mouse_006.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/K7DXe5W4bthmwzvE4ikgx3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Signature Comfort Plus M850L is a mighty comfortable mouse, and before diving into the overall comfort factor, I want to talk about its headline feature. This is Logitech’s first mouse to feature a built-in palm cushion, located right behind the left- and right-click buttons.</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="uhPKSpvjkPP9NS8NrrWH74" name="Logi_M850L_Mouse_010.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/uhPKSpvjkPP9NS8NrrWH74-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 M850L’s palm cushion is made of durable, squishy material. It’s designed to provide some ergonomic relief by reducing pressure on the main bit of your palm — and it works wonders. After two whole days of using the M850L, I didn’t feel any discomfort. Combined with the soft rubber grips that my thumb rested on, and the contoured shape which enabled my hand to sit more naturally on the mouse, the M850L is a killer rodent for the office.</p><h2 id="a-productivity-beast">A productivity beast</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zkJZyACPFUyeHzrzMsf5j3" name="Logi_M850L_Mouse_011.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/zkJZyACPFUyeHzrzMsf5j3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Signature Comfort Plus M850L is a beast for productivity. It has a maximum DPI of 4,000 which is adjustable via the button located between the left- and right-click ones. Though this isn’t anything special for gaming , it’s more than enough for those who want a speedy work-first pointer — after all, this isn’t a gaming mouse. Polling-wise, I didn’t notice any input lag or latency issues over Bluetooth, which will max out at 125Hz. This won't be enough for competitive gaming, and with no 2.4G connection as standard (unless you buy the receiver), there's no option for rapid poll rates out the box. Again, though, this isn't a gaming mouse, so I can forgive it. For a budget productivity/gaming mouse, check out the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69), which polls at up to 8K.</p><p>Thanks to two PTFE skates on the mouse’s underside, the M850L glides effortlessly across most surfaces — including plastic, glass and rubber desk mats. One of the things I love most about the M850L is its silent microswitches designed to reduce noise by up to 90%. This means that your coworkers won’t be disturbed by your clicking the mouse’s buttons — and neither will you, and this can help maintain your concentration on the task at hand.</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="nPEaJQRaeE7kkYMZcUbLq3" name="Logi_M850L_Mouse_014.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/nPEaJQRaeE7kkYMZcUbLq3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 haven’t even talked about the best part yet, though, and that’s the M850L’s SmartWheel. Logi’s SmartWheel technology isn’t brand new, per se, and it’s found primarily on the brand’s productivity mice — and I love it. The scroll wheel on the M850L is speed-adaptive, which means that it automatically switches between two scrolling modes based on how fast you’re spinning it. You don’t need to manually switch between tactile (ratchet) and super-fast (free-spin) scrolling, like you need to on the <a href="https://www.tomsguide.com/computing/peripherals/razer-pro-click-mini-review">Razer Pro Click Mini</a> ($79).</p><p>I’ve thoroughly enjoyed using the M850L’s SmartWheel. I deal with many lengthy spreadsheets at work, so being able to go from the top to the bottom of a Google Sheet speedily and within seconds is a boon. Similarly, scrolling through hundreds of unread messages on Slack channels is a walk in the park too. And when I need to, I can simply stop the scroll wheel from spinning to stop at a particular section. Easy as pie.</p><h2 id="user-friendly-companion-software-4">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/m9NnAb8CQgLNYM5SG8a3wR-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/EyBdvPL4A2t2tRgw4bRQ2S-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure></figure><p>Much like many other Logitech office peripherals, the Logitech Signature Comfort Plus M850L is compatible with <a href="https://www.logitech.com/en-gb/software/logi-options-plus" target="_blank" rel="nofollow">Logi Options+</a>. This is perhaps one of my favorite software suites as it’s extremely user-friendly and intuitive, and it sports a clean interface with no intrusive pop-ups or ads. Logi Options+ is also compatible with both Windows and macOS, unlike the Windows-only app for the similarly priced <a href="https://www.tomsguide.com/computing/peripherals/lenovo-go-wireless-vertical-review">Lenovo Go Wireless Vertical</a> ($49).</p><p>Logi Options+ enables you to customize the M850L effortlessly and extensively. You can remap the six buttons, assign Gestures to the DPI button, such as media controls and app navigation, and assign app-specific functions too. For instance, you can customize the side buttons to adjust the volume in Spotify, copy/paste in Microsoft Word, or undo/redo in Adobe Photoshop. I find the last especially useful as I do a lot of editing in Photoshop.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/EPHwHx7w5cAjkhFnVyjsxR-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/u6VTG78BBevJr8paEBZDxR-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure></figure><p>In addition to all of this, you can also toggle Flow which lets you move the mouse cursor from one computer to another. Naturally, you’ll need to install Logi Options+ on both machines to get this working. I did this with my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> and the <a href="https://www.tomsguide.com/computing/laptops/geekom-geekbook-x14-pro-review">Geekom GeekBook X14 Pro</a>, and it worked like a charm. I was able to copy text on my MacBook and then paste it in a Google Docs window on the Geekom. This negates the need to swap Bluetooth sources every time.</p><h2 id="competitively-priced-4">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DBQMLEgDzBwRWHRZ9fVst3" name="Logi_M850L_Mouse_008.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/DBQMLEgDzBwRWHRZ9fVst3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 certainly not least, the Logitech Signature Comfort Plus M850L is a very affordable mouse, making it perfect for office workers on a budget. It retails for just <a href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49 at Amazon</a> — or you can grab the Combo with the MK880 keyboard for <a href="https://www.amazon.com/Logitech-Signature-Wireless-Full-Size-Bluetooth/dp/B0GCRT8RTH/" target="_blank" rel="nofollow">$99 at Amazon U.S.</a> / <a href="https://www.currys.co.uk/products/logitech-signature-comfort-plus-mk880-wireless-keyboard-and-mouse-set-graphite-10301889.html" target="_blank" rel="nofollow">£99 at Currys U.K.</a> Again, really affordable.</p><p>The M850L is slightly pricier than the <a href="https://www.tomsguide.com/computing/peripherals/esr-magmouse-review">ESR MagMouse</a> ($34) but you’re getting unparalleled comfort and an overall better package. Other similarly priced mice include the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m7-review">Keychron M7</a> ($49) but that’s predominantly a gaming mouse, and the M850L’s build quality feels more top-notch. The M850L is also arguably better than the productivity-oriented Razer Pro Click Mini which features a noisy (therefore, annoying) scroll wheel.</p><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-the-downs"><span>Logitech Signature Comfort Plus M850L review: The downs</span></h2><p>It’s hard to fault the Logitech Signature Comfort Plus M850L, and the only real downsides to this mouse are that it uses disposable batteries and doesn’t come with a 2.4GHz receiver.</p><h2 id="uses-disposable-batteries">Uses disposable batteries</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tnC462AbEDzNj29hpumec3" name="Logi_M850L_Mouse_012.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/tnC462AbEDzNj29hpumec3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 probably sound like a broken record at this point to those who have read my previously published mice reviews, but I’m here to talk about my dislike for disposable batteries again. The Logitech Signature Comfort Plus M850L utilizes a disposable AA battery, and that might be a dealbreaker for the environmentally conscious.</p><p>Disposable batteries are tricky to recycle and they create massive amounts of single-use waste. The upside, for those who wish to see it, is that the M850L can last up to 24 months on one such battery. If you want a mouse with a built-in rechargeable battery instead, I’d recommend the Keychron M7, or splurging more on the <a href="https://www.tomsguide.com/reviews/logitech-lift">Logitech Lift </a>($69) and the <a href="https://www.tomsguide.com/computing/mice/logitech-mx-master-4-review">Logitech MX Master 4</a> ($119).</p><h2 id="logi-bolt-usb-receiver-sold-separately">Logi Bolt USB receiver sold separately</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3r76KGCt33fhTVdY9etjy3" name="Logi_M850L_Mouse_005.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/3r76KGCt33fhTVdY9etjy3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Unlike the Logitech MX Master 4, the Logitech Signature Comfort Plus M850L doesn’t come with a Logi Bolt USB receiver. If you want to use the mouse with a dongle plugged in, you’ll need to buy it separately. It isn’t too expensive, available for just <a href="https://www.amazon.com/Logitech-956-000007-LOGITECH-Bolt-Receiver/dp/B09JL9Y1WS/" target="_blank" rel="nofollow">$14</a> / <a href="https://www.amazon.co.uk/Logitech-956-000007-LOGITECH-Bolt-Receiver/dp/B09JL9Y1WS/" target="_blank" rel="nofollow">£11 at Amazon</a>.</p><p>For those who own multiple Logi products, the Bolt receiver is a great investment as it enables you to connect multiple keyboards and mice to your computer without needing separate dongles. It could also allow for higher poll rates than Bluetooth. It's a shame not to see it included as standard.</p><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-verdict"><span>Logitech Signature Comfort Plus M850L 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="6QkwX9Bo2VspAVmNnzhqL4" name="Logi_M850L_Mouse_004.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/6QkwX9Bo2VspAVmNnzhqL4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 don’t make sub-$50 mice better than this. The Logitech Signature Comfort Plus M850L feels like a no-brainer if you have a sedentary job. Its standout feature, the integrated cushioned palm rest, genuinely enhances long-term comfort, as does its contoured shape and soft rubber grips for your thumb.</p><p>Combine this with quiet clicks, Logi’s excellent SmartWheel speed-adaptive scrolling, smooth tracking, and extensive customization via Logi Options+, and you’ve got yourself a winning package.</p><p>Though the use of disposable batteries and exclusion of a 2.4GHz receiver in the box are a little disappointing, neither drawback is a dealbreaker. For office or remote workers seeking a reliable, comfortable and feature-rich rodent on a budget, the M850L is unbeatable value for money.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/mice/logitech-signature-comfort-plus-m850l-review</link>
                                                                            <description>
                            <![CDATA[ The Logitech M850L is the brand's first mouse with a built-in cushioned palm rest which delivers unparalleled comfort for all-day work, and it costs just $49. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">MLxukySeKYfZsGGRacrvUV</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/XvD8buBhQcdm7c3fZiV5f3-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Wed, 03 Jun 2026 09:54:01 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:41 +0000</updated>
                                                                                                                                            <category><![CDATA[Mice]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/XvD8buBhQcdm7c3fZiV5f3-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Logitech Signature Comfort Plus M850L mouse]]></media:description>                                                            <media:text><![CDATA[A Logitech Signature Comfort Plus M850L mouse]]></media:text>
                                <media:title type="plain"><![CDATA[A Logitech Signature Comfort Plus M850L mouse]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/XvD8buBhQcdm7c3fZiV5f3-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Few manufacturers are better than Logitech when it comes to creating comfortable productivity gadgets, and the Swiss-American giant has done it once again. The Logitech Signature Comfort Plus M850L is unlike any other Logi mouse as it features a built-in cushioned palm rest that delivers all-day comfort.</p><p>The M850L is a beast for productivity, thanks to its speed-adaptive smart scroll wheel, quiet clicks, and extensive customization capabilities. But it isn’t perfect — few things in life are. The mouse uses disposable batteries and doesn’t come with a 2.4GHz receiver. Are either of those dealbreakers? Not at all, given that the mouse costs just $49.</p><p>To find out if this is the <a href="https://www.tomsguide.com/best-picks/best-mouse">best mouse</a> for you, read my full Logitech Signature Comfort Plus M850L review.</p><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-specs"><span>Logitech Signature Comfort Plus M850L 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/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>4,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>6</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.79 x 2.92 x 1.74 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>3.8oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, Graphite, Off-White</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, Logi Bolt USB Receiver</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>1 x AA</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>24 months</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-cheat-sheet"><span>Logitech Signature Comfort Plus M850L review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A right-handed rodent and Logitech’s first mouse with an integrated palm cushion</li><li><strong>Who is it for?</strong> For sedentary workers needing a comfortable mouse</li><li><strong>How much does it cost? </strong>The Logitech Signature Comfort Plus M850L is available for just <a href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49</a></li><li><strong>What do we like?</strong> Its built-in cushioned palm rest, overall comfort, user-friendly companion software, quiet clicks, and speed-adaptive scroll wheel</li><li><strong>What don’t we like? </strong>It uses disposable batteries and doesn’t come with a 2.4GHz receiver</li></ul><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-the-ups"><span>Logitech Signature Comfort Plus M850L review: The ups</span></h2><p>From its eco-friendly design, built-in cushioned palm rest, quiet clicks and speed-adaptive scroll wheel, the Logitech Signature Comfort Plus M850L is perhaps the best budget productivity mouse you can buy today.</p><h2 id="low-key-and-eco-friendly-design">Low-key and eco-friendly 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="xptrj3v79yLMf4RUbctVj3" name="Logi_M850L_Mouse_003.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/xptrj3v79yLMf4RUbctVj3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Signature Comfort Plus M850L is a productivity-oriented mouse for the office, and it’s designed as such. It sports a minimalist, low-key design that doesn’t draw eyeballs to your desk thanks to the lack of any flashy RGB or lighting. The mouse is available in three colors: Graphite, Black, and Off-White. I tested the Graphite model which is covered in a couple shades of light and dark gray, and it looks fantastic.</p><p>Measuring 4.79 x 2.92 x 1.74 inches, the M850L is ideal for those with small- or medium-sized hands. It weighs 3.8 ounces so it’s got a bit of heft to it, which is nice — it makes the mouse feel sturdy and substantial when your hand is cupping it. The textured surface that runs along the length of the body gives your thumb a good grip over the mouse. I’ll discuss the overall comfort in the following section.</p><p>The M850L feels extremely well-built, and you can tell that the plastic it utilizes is of high quality. I also like that the Graphite and Black models are made of 63% recycled material (49% on the Off-White version), and the mouse comes in paper packaging derived from <a href="https://uk.fsc.org/what-is-fsc/fsc-certified-forests" target="_blank" rel="nofollow">FSC-certified forests</a>.</p><h2 id="all-day-comfort">All-day comfort</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="K7DXe5W4bthmwzvE4ikgx3" name="Logi_M850L_Mouse_006.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/K7DXe5W4bthmwzvE4ikgx3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Signature Comfort Plus M850L is a mighty comfortable mouse, and before diving into the overall comfort factor, I want to talk about its headline feature. This is Logitech’s first mouse to feature a built-in palm cushion, located right behind the left- and right-click buttons.</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="uhPKSpvjkPP9NS8NrrWH74" name="Logi_M850L_Mouse_010.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/uhPKSpvjkPP9NS8NrrWH74-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 M850L’s palm cushion is made of durable, squishy material. It’s designed to provide some ergonomic relief by reducing pressure on the main bit of your palm — and it works wonders. After two whole days of using the M850L, I didn’t feel any discomfort. Combined with the soft rubber grips that my thumb rested on, and the contoured shape which enabled my hand to sit more naturally on the mouse, the M850L is a killer rodent for the office.</p><h2 id="a-productivity-beast">A productivity beast</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="zkJZyACPFUyeHzrzMsf5j3" name="Logi_M850L_Mouse_011.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/zkJZyACPFUyeHzrzMsf5j3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Logitech Signature Comfort Plus M850L is a beast for productivity. It has a maximum DPI of 4,000 which is adjustable via the button located between the left- and right-click ones. Though this isn’t anything special for gaming , it’s more than enough for those who want a speedy work-first pointer — after all, this isn’t a gaming mouse. Polling-wise, I didn’t notice any input lag or latency issues over Bluetooth, which will max out at 125Hz. This won't be enough for competitive gaming, and with no 2.4G connection as standard (unless you buy the receiver), there's no option for rapid poll rates out the box. Again, though, this isn't a gaming mouse, so I can forgive it. For a budget productivity/gaming mouse, check out the <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69), which polls at up to 8K.</p><p>Thanks to two PTFE skates on the mouse’s underside, the M850L glides effortlessly across most surfaces — including plastic, glass and rubber desk mats. One of the things I love most about the M850L is its silent microswitches designed to reduce noise by up to 90%. This means that your coworkers won’t be disturbed by your clicking the mouse’s buttons — and neither will you, and this can help maintain your concentration on the task at hand.</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="nPEaJQRaeE7kkYMZcUbLq3" name="Logi_M850L_Mouse_014.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/nPEaJQRaeE7kkYMZcUbLq3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 haven’t even talked about the best part yet, though, and that’s the M850L’s SmartWheel. Logi’s SmartWheel technology isn’t brand new, per se, and it’s found primarily on the brand’s productivity mice — and I love it. The scroll wheel on the M850L is speed-adaptive, which means that it automatically switches between two scrolling modes based on how fast you’re spinning it. You don’t need to manually switch between tactile (ratchet) and super-fast (free-spin) scrolling, like you need to on the <a href="https://www.tomsguide.com/computing/peripherals/razer-pro-click-mini-review">Razer Pro Click Mini</a> ($79).</p><p>I’ve thoroughly enjoyed using the M850L’s SmartWheel. I deal with many lengthy spreadsheets at work, so being able to go from the top to the bottom of a Google Sheet speedily and within seconds is a boon. Similarly, scrolling through hundreds of unread messages on Slack channels is a walk in the park too. And when I need to, I can simply stop the scroll wheel from spinning to stop at a particular section. Easy as pie.</p><h2 id="user-friendly-companion-software-4">User-friendly companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/m9NnAb8CQgLNYM5SG8a3wR-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/EyBdvPL4A2t2tRgw4bRQ2S-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure></figure><p>Much like many other Logitech office peripherals, the Logitech Signature Comfort Plus M850L is compatible with <a href="https://www.logitech.com/en-gb/software/logi-options-plus" target="_blank" rel="nofollow">Logi Options+</a>. This is perhaps one of my favorite software suites as it’s extremely user-friendly and intuitive, and it sports a clean interface with no intrusive pop-ups or ads. Logi Options+ is also compatible with both Windows and macOS, unlike the Windows-only app for the similarly priced <a href="https://www.tomsguide.com/computing/peripherals/lenovo-go-wireless-vertical-review">Lenovo Go Wireless Vertical</a> ($49).</p><p>Logi Options+ enables you to customize the M850L effortlessly and extensively. You can remap the six buttons, assign Gestures to the DPI button, such as media controls and app navigation, and assign app-specific functions too. For instance, you can customize the side buttons to adjust the volume in Spotify, copy/paste in Microsoft Word, or undo/redo in Adobe Photoshop. I find the last especially useful as I do a lot of editing in Photoshop.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/EPHwHx7w5cAjkhFnVyjsxR-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/u6VTG78BBevJr8paEBZDxR-1920-80.jpg" alt="Logi Options+ for customizing the Logitech M850L mouse" /><figcaption><small role="credit">Logitech / Tom's Guide</small></figcaption></figure></figure><p>In addition to all of this, you can also toggle Flow which lets you move the mouse cursor from one computer to another. Naturally, you’ll need to install Logi Options+ on both machines to get this working. I did this with my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> and the <a href="https://www.tomsguide.com/computing/laptops/geekom-geekbook-x14-pro-review">Geekom GeekBook X14 Pro</a>, and it worked like a charm. I was able to copy text on my MacBook and then paste it in a Google Docs window on the Geekom. This negates the need to swap Bluetooth sources every time.</p><h2 id="competitively-priced-4">Competitively priced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="DBQMLEgDzBwRWHRZ9fVst3" name="Logi_M850L_Mouse_008.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/DBQMLEgDzBwRWHRZ9fVst3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 certainly not least, the Logitech Signature Comfort Plus M850L is a very affordable mouse, making it perfect for office workers on a budget. It retails for just <a href="https://www.amazon.com/Logitech-Signature-Wireless-Bluetooth-Customizable/dp/B0GSHRCS7L/" target="_blank" rel="nofollow">$49</a> / <a href="https://www.amazon.co.uk/Logitech-Signature-Wireless-Bluetooth-Customisable/dp/B0GVF643TR/" target="_blank" rel="nofollow">£49 at Amazon</a> — or you can grab the Combo with the MK880 keyboard for <a href="https://www.amazon.com/Logitech-Signature-Wireless-Full-Size-Bluetooth/dp/B0GCRT8RTH/" target="_blank" rel="nofollow">$99 at Amazon U.S.</a> / <a href="https://www.currys.co.uk/products/logitech-signature-comfort-plus-mk880-wireless-keyboard-and-mouse-set-graphite-10301889.html" target="_blank" rel="nofollow">£99 at Currys U.K.</a> Again, really affordable.</p><p>The M850L is slightly pricier than the <a href="https://www.tomsguide.com/computing/peripherals/esr-magmouse-review">ESR MagMouse</a> ($34) but you’re getting unparalleled comfort and an overall better package. Other similarly priced mice include the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m7-review">Keychron M7</a> ($49) but that’s predominantly a gaming mouse, and the M850L’s build quality feels more top-notch. The M850L is also arguably better than the productivity-oriented Razer Pro Click Mini which features a noisy (therefore, annoying) scroll wheel.</p><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-the-downs"><span>Logitech Signature Comfort Plus M850L review: The downs</span></h2><p>It’s hard to fault the Logitech Signature Comfort Plus M850L, and the only real downsides to this mouse are that it uses disposable batteries and doesn’t come with a 2.4GHz receiver.</p><h2 id="uses-disposable-batteries">Uses disposable batteries</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tnC462AbEDzNj29hpumec3" name="Logi_M850L_Mouse_012.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/tnC462AbEDzNj29hpumec3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 probably sound like a broken record at this point to those who have read my previously published mice reviews, but I’m here to talk about my dislike for disposable batteries again. The Logitech Signature Comfort Plus M850L utilizes a disposable AA battery, and that might be a dealbreaker for the environmentally conscious.</p><p>Disposable batteries are tricky to recycle and they create massive amounts of single-use waste. The upside, for those who wish to see it, is that the M850L can last up to 24 months on one such battery. If you want a mouse with a built-in rechargeable battery instead, I’d recommend the Keychron M7, or splurging more on the <a href="https://www.tomsguide.com/reviews/logitech-lift">Logitech Lift </a>($69) and the <a href="https://www.tomsguide.com/computing/mice/logitech-mx-master-4-review">Logitech MX Master 4</a> ($119).</p><h2 id="logi-bolt-usb-receiver-sold-separately">Logi Bolt USB receiver sold separately</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3r76KGCt33fhTVdY9etjy3" name="Logi_M850L_Mouse_005.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/3r76KGCt33fhTVdY9etjy3-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Unlike the Logitech MX Master 4, the Logitech Signature Comfort Plus M850L doesn’t come with a Logi Bolt USB receiver. If you want to use the mouse with a dongle plugged in, you’ll need to buy it separately. It isn’t too expensive, available for just <a href="https://www.amazon.com/Logitech-956-000007-LOGITECH-Bolt-Receiver/dp/B09JL9Y1WS/" target="_blank" rel="nofollow">$14</a> / <a href="https://www.amazon.co.uk/Logitech-956-000007-LOGITECH-Bolt-Receiver/dp/B09JL9Y1WS/" target="_blank" rel="nofollow">£11 at Amazon</a>.</p><p>For those who own multiple Logi products, the Bolt receiver is a great investment as it enables you to connect multiple keyboards and mice to your computer without needing separate dongles. It could also allow for higher poll rates than Bluetooth. It's a shame not to see it included as standard.</p><h2 class="article-body__section" id="section-logitech-signature-comfort-plus-m850l-review-verdict"><span>Logitech Signature Comfort Plus M850L 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="6QkwX9Bo2VspAVmNnzhqL4" name="Logi_M850L_Mouse_004.JPG" alt="A Logitech Signature Comfort Plus M850L mouse" src="https://cdn.mos.cms.futurecdn.net/6QkwX9Bo2VspAVmNnzhqL4-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 don’t make sub-$50 mice better than this. The Logitech Signature Comfort Plus M850L feels like a no-brainer if you have a sedentary job. Its standout feature, the integrated cushioned palm rest, genuinely enhances long-term comfort, as does its contoured shape and soft rubber grips for your thumb.</p><p>Combine this with quiet clicks, Logi’s excellent SmartWheel speed-adaptive scrolling, smooth tracking, and extensive customization via Logi Options+, and you’ve got yourself a winning package.</p><p>Though the use of disposable batteries and exclusion of a 2.4GHz receiver in the box are a little disappointing, neither drawback is a dealbreaker. For office or remote workers seeking a reliable, comfortable and feature-rich rodent on a budget, the M850L is unbeatable value for money.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I used Keychron’s new split mechanical keyboard for a week and its tether-free design is a complete game changer ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Just when I thought I’d never go back to using a <a href="https://www.tomsguide.com/computing/peripherals/i-finally-tried-magnetic-key-switches-but-i-never-thought-id-find-them-in-a-split-keyboard-thats-also-wireless">split mechanical keyboard</a>, Keychron’s new Q11 Ultra came along and fixed everything I disliked about my previous one while taking the typing experience to a whole new level.</p><p>Switching from a mushy membrane keyboard to one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> was one of the biggest upgrades I’ve ever made. Doing so not only improved my typing, but it also helped reduce hand fatigue. There was just one problem: ergonomics.</p><p>That’s where split mechanical keyboards come in. As the name suggests, these keyboards are made up of two halves that work together but can be spread apart on your desk. This way, your arms rest at a more natural position while typing, keeping them shoulder-width apart.</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/njtcgcqZWXk" allowfullscreen></iframe></div></div><p>For most people, using a standard mechanical keyboard will still be a massive improvement over a membrane one. Given that I write for a living and type a lot more than most people do each day, I wanted to take things to the next level.</p><p>Now, the Q11 Ultra isn’t the first split mechanical keyboard I’ve used. However, it has one big upgrade that I’ve been waiting to try out for myself. After using Keychron’s latest split keyboard for a week, I don’t know how I can go back to a standard mechanical keyboard.</p><div class="product"><a data-dimension112="a194218a-460d-460c-a495-2db6c240493b" data-action="Deal Block" data-label="Q11 Ultra" data-dimension48="Q11 Ultra" data-dimension25="$239" href="https://www.amazon.com/Keychron-Ergonomic-Mechanical-Open-Source-Hot-Swappable/dp/B0GQT4YC5H?th=1" 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:75.33%;"><img id="ELTWHiYVeNbjqoR7GP4zX9" name="Q11 Ultra" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ELTWHiYVeNbjqoR7GP4zX9-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1130" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>This 75% split mechanical keyboard features an incredibly premium full-metal chassis and an ultra-fast 8,000Hz wireless polling rate. Its completely tether-free design allows you to position the two halves wherever they feel most comfortable on your desk, while Keychron's hot-swappable Silk POM switches offer a self-lubricating, ultra-smooth typing experience that actively helps reduce hand fatigue. The Q11 Ultra also comes with extra switches, a keycap puller, and dual customizable control knobs.</p><p><strong>Price Check:</strong> <a href="https://www.keychron.com/products/keychron-q11-ultra-8k-wireless-custom-mechanical-keyboard" target="_blank" rel="nofollow" data-dimension112="a194218a-460d-460c-a495-2db6c240493b" data-action="Deal Block" data-label="Q11 Ultra" data-dimension48="Q11 Ultra" data-dimension25="$239">$239 @ Keychron</a><a class="view-deal button" href="https://www.amazon.com/Keychron-Ergonomic-Mechanical-Open-Source-Hot-Swappable/dp/B0GQT4YC5H?th=1" target="_blank" rel="nofollow" data-dimension112="a194218a-460d-460c-a495-2db6c240493b" data-action="Deal Block" data-label="Q11 Ultra" data-dimension48="Q11 Ultra" data-dimension25="$239">View Deal</a></p></div><h2 id="a-premium-keyboard-with-a-twist">A premium keyboard with a twist</h2><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="A7oRonmK7CJgubP62qZoWZ" name="Keychron Q11 Ultra-6" alt="The Keychron Q11 Ultra unboxed on a desk" src="https://cdn.mos.cms.futurecdn.net/A7oRonmK7CJgubP62qZoWZ-1920-80.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 Q11 Ultra comes with plenty of useful accessories in the box. From Windows-specific keycaps to extra mechanical switches and a USB-C to USB-A adapter, you have everything you need to get started and to tweak this keyboard to your liking.</p><p>Just like with the <a href="https://www.tomsguide.com/computing/keyboards/i-tried-the-new-keychron-q1-ultra-8k-and-this-hefty-full-featured-keyboard-is-a-game-changer-for-my-workflow">Keychron Q1 Ultra</a> I tried earlier this year, the Q11 Ultra is an 8K mechanical keyboard with a premium design and build. This higher polling rate is often associated with the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> but there’s a benefit for fast typists too, as it results in a very responsive, fluid, and connected feeling.</p><p>In the past, you’d have to use the included cable to experience an 8000 Hz polling rate. This isn’t the case with the Q11 Ultra though, as you can do so wirelessly using the included 2.4 GHz USB dongle. This just so happens to be my preferred connection method too, since I’m always switching between computers while reviewing the <a href="https://www.tomsguide.com/us/best-mini-pc.html">best mini PCs</a>. Instead of dealing with Bluetooth pairing, I just plug in the dongle and I’m good to go.</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="yoUFwXnMRCi84ueiZGQAT3" name="Keychron Q11 Ultra-5" alt="The Keychron Q11 Ultra on a desk with both halves connected together" src="https://cdn.mos.cms.futurecdn.net/yoUFwXnMRCi84ueiZGQAT3-1920-80.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 Q11 Ultra features KSA double-shot PBT keycaps like on the Q1 Ultra. They look great and feel just as good underneath your fingertips. Besides being a split keyboard, the Q11 Ultra has some other unique features too. For instance, instead of just one knob on the top right side of the keyboard, there’s a second knob on the left side too. Out of the box, both are set to control your computer’s volume while pressing the knob down mutes your audio. However, in the web-based <a href="https://launcher.keychron.com/#/not-connect">Keychron Launcher</a>, you can customize them and the rest of the keyboard’s keys. Likewise, you’ll need to use it to activate the Q11 Ultra’s faster 8K polling rate too.</p><p>Although the Q11 Ultra is a 75% keyboard with a function row up top and arrow keys in the bottom right corner, it does have a few extra macro keys on the left side. Within the Keychron Launcher, you can program M1-M5 any way you want. For instance, I currently have M1 set to open a new File Explorer in Windows while I have M2 set to open Task View so I can easily switch between virtual desktops. The possibilities are basically endless since you can also record your own macros — or multi-key shortcuts — and assign them to any of these M keys 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="3uayeVdj5RvN3Mws6tuMd8" name="Keychron Q11 Ultra-4" alt="The backside of the Keychron Q11 Ultra showing off its power and input mode toggles along with its USB-C ports" src="https://cdn.mos.cms.futurecdn.net/3uayeVdj5RvN3Mws6tuMd8-1920-80.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>Now around back is where things get interesting with the Q11 Ultra. On the left half, you have a USB-C charging port and two toggle switches: one to switch between connection modes (Bluetooth, wired and 2.4 GHz) and another to switch between Mac or Windows. Then on the right half, you have another USB-C charging port for that side of the Q11 Ultra and a simple on/off switch. This switch can come in handy if you just want to play one of the <a href="https://www.tomsguide.com/best-picks/best-PC-games">best PC games</a> with just the left side of the keyboard to make more room for your mouse.</p><p>With the Q11 Ultra unboxed, it was time to bring it over to my desk and get it set up.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-O6jjGO"></div>                            </div>                            <script src="https://kwizly.com/embed/O6jjGO.js" async></script><h2 id="a-no-compromise-split">A no-compromise split</h2><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="iqEh2dGs8k8jrCR25BTRnC" name="Keychron Q11 Ultra-3" alt="A desk setup built around the Keychron Q11 Ultra and the Geekom A9 Max 2026 Edition" src="https://cdn.mos.cms.futurecdn.net/iqEh2dGs8k8jrCR25BTRnC-1920-80.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>Since I’m in the process of reviewing the 2026 Edition of the <a href="https://www.tomsguide.com/computing/desktop-computers/mini-pcs/geekom-a9-max-review">Geekom A9 Max</a>, I decided to build a whole productivity-focused desk setup around it with the Q11 Ultra front and center. On the right, instead of my trusty <a href="https://www.tomsguide.com/reference/what-is-a-trackball-mouse">trackball mouse</a>, I’ve paired this keyboard with the Keychron M5 vertical mouse I’m also testing out. Then in the center above the A9 Max 2026 Edition, I have the <a href="https://www.tomsguide.com/computing/monitors/i-ditched-my-dual-monitor-setup-for-a-single-40-inch-ultrawide-and-two-screens-arent-better-than-one">Innocn 40C1U</a> 40-inch ultrawide monitor for as much screen real estate as I can get without having to go from a flat to a curved display. And if you’re wondering about the desk, this is the <a href="https://www.tomsguide.com/home/home-office/uplift-v3-standing-desk-review">Uplift V3</a> standing desk with power grommets on either side.</p><p>Although the monitor and the mini PC powering this whole setup are excellent in their own right, it’s the Q11 Ultra that’s made the biggest difference. It feels just as good to type on as the Q1 Ultra since I’m also using Keychron’s own Silk POM Banana switches, which feature an early tactile bump compared to traditional brown mechanical switches. When combined with the KSA double-shot PBT keycaps and the Q11 Ultra’s full metal body, the whole experience of using this keyboard has an incredibly premium feel to it.</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="JHqG6SZYPiFZbaWhdoD9VG" name="Keychron Q11 Ultra-2" alt="A person typing on the Keychron Q11 Ultra split mechanical keyboard at their desk" src="https://cdn.mos.cms.futurecdn.net/JHqG6SZYPiFZbaWhdoD9VG-1920-80.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 best part about the Q11 Ultra is its tether-free design. In the past, the two halves of a split mechanical keyboard needed to be connected to one another using a short cable. With the Q11 Ultra, Keychron uses advanced 2.4 GHz wireless sync to allow the two halves to communicate seamlessly without one. What I like about this is that you can have the two halves of this split keyboard as far apart or as close to one another as you’d like. Although I prefer having the two halves aligned, you can also tilt them inward or outward depending on your preference.</p><p>Keychron has also made a lot of improvements to the acoustic profile of the Q11 Ultra compared to the original <a href="https://www.tomsguide.com/computing/peripherals/keychron-q11-review">Q11</a>. While you just had one layer of sound-absorbing foam with the latter, this upgraded model has two layers of foam between its switches and PCB, as well as two more at the very bottom. As such, the Q11 Ultra sounds just as good while typing as it feels underneath your fingertips.</p><p>Even though I’ve kept it off for the most part, you also have full RGB backlighting as well as per-key RGB, where you can individually customize the color of each key. Just like with 8K polling, leaving the Q11 Ultra’s RGB backlighting on will drain its battery faster. Speaking of battery life, Keychron says you can get up to 160 hours on a single charge. I’ve been using the Q11 Ultra as my daily driver for over a week now and I haven’t had to charge it yet.</p><div class="product"><a data-dimension112="13353d62-0bfc-43fc-89e8-3ca916ff0cd1" data-action="Deal Block" data-label="Split70" data-dimension48="Split70" data-dimension25="$99" href="https://www.amazon.com/EPOMAKER-Split70-Wireless-Ergonomic-Mechanical/dp/B0FPQX172M" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1479px;"><p class="vanilla-image-block" style="padding-top:45.57%;"><img id="Nvxjk6tqpBJWRhEEyfSyA6" name="Split70" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Nvxjk6tqpBJWRhEEyfSyA6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1479" height="674" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/EPOMAKER-Split70-Wireless-Ergonomic-Mechanical/dp/B0FPQX172M" target="_blank" rel="nofollow" data-dimension112="13353d62-0bfc-43fc-89e8-3ca916ff0cd1" data-action="Deal Block" data-label="Split70" data-dimension48="Split70" data-dimension25="$99">Split70: was $115 now $99</a></strong><br>This 70% split mechanical keyboard is a budget-friendly alternative that features flexible tri-mode connectivity (Bluetooth, 2.4GHz wireless, and wired), hot-swappable switches, and fully programmable keys via VIA software customization. It also features a gasket-mounted design, a programmable rotary knob, RGB backlighting and pre-lubed switches right out of the box.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Split70-Wireless-Ergonomic-Mechanical/dp/B0FPQX172M" target="_blank" rel="nofollow" data-dimension112="13353d62-0bfc-43fc-89e8-3ca916ff0cd1" data-action="Deal Block" data-label="Split70" data-dimension48="Split70" data-dimension25="$99">View Deal</a></p></div><h2 id="tether-free-finally">Tether-free, finally</h2><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="5oHiKwxWuZ46SS6i9jqW2S" name="Seenda Upgraded Trackpad-5.jpg" alt="A picture of a desk setup with a trackpad on the left side of a split mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/5oHiKwxWuZ46SS6i9jqW2S-1920-80.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>Back when I had my first split mechanical keyboard — the <a href="https://www.tomsguide.com/opinion/i-added-a-mini-monitor-to-my-desk-setup-and-its-changed-how-i-work-heres-how">Mistel Barocco</a> — almost everything except for my mouse was connected to my PC via wires. Sure, it was dependable and lag-free, but as my <a href="https://www.tomsguide.com/computing/i-screenmaxxed-my-desk-setup-by-going-from-three-to-five-displays-but-they-arent-all-monitors">desk setups</a> have evolved, having wires running across the top of my desk just didn’t do all the work I put into my <a href="https://www.tomsguide.com/opinion/i-spent-a-year-reviewing-standing-desks-and-this-is-the-one-accessory-i-cant-live-without">under-desk cable management</a> justice.</p><p>After picking up the <a href="https://www.tomsguide.com/computing/peripherals/i-spent-just-dollar30-on-a-mechanical-keyboard-to-prove-you-dont-need-to-waste-your-money">Keychron C3 Pro</a> for just $30 on a whim a few years ago to see how far budget mechanical keyboards had come, I fell for Keychron, and I fell hard. While I initially gravitated toward its low-profile mechanical options, getting a chance to try the Q1 Ultra recently really made me appreciate its full-profile boards.</p><p>I almost picked up the original Q11 several times over the years, but in retrospect, I’m glad I waited for the Q11 Ultra. Keychron’s Banana switches feel like a massive improvement over traditional MX Browns, and its tether-free design is something I never thought I’d see at this price point. At $239, the Q11 Ultra is far from a starter board, but given what premium mechanical setups used to cost only a few years ago, it’s an absolute steal for this level of design and wireless functionality.</p><p>If you’ve already invested in one of the <a href="https://www.tomsguide.com/home/home-office/best-office-chairs">best office chairs</a> and a solid <a href="https://www.tomsguide.com/home/home-office/best-standing-desks">standing desk</a> to go alongside it, a split mechanical keyboard could be the last piece of the ergonomic puzzle you’ve been missing. I used to think a standard mechanical layout was good enough for me too, but after spending a week daily driving the Q11 Ultra, going back is going to be incredibly tough.</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/computing/keyboards/ive-used-the-same-usd30-logitech-keyboard-for-10-years-and-framework-just-showed-off-the-upgrade-i-actually-want">I've used the same $30 Logitech keyboard for 10 years and Framework just showed off the upgrade I actually want</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-tested-the-worlds-first-concrete-keyboard-theres-a-reason-nobody-else-does-this">I tested the world’s first concrete keyboard and there’s a reason nobody else does this</a></li><li><a href="https://www.tomsguide.com/computing/i-ditched-my-laptop-for-a-pocketable-mini-pc-and-a-pair-of-ar-glasses-heres-what-happened">I gave my favorite pocketable mini PC an instant upgrade with this docking station — and now it’s a full-fledged gaming rig</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/i-used-keychrons-new-split-mechanical-keyboard-for-a-week-and-its-tether-free-design-is-a-complete-game-changer</link>
                                                                            <description>
                            <![CDATA[ Keychron’s new Q11 Ultra is the premium, tether-free split mechanical keyboard I’ve been waiting for to completely change my workflow. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">LgyGJ2MmY9wnxiXfuViSwe</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/vMGpmubyydhZ3u2CbUbHMW-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 30 May 2026 10:30:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                                                                <author><![CDATA[ anthony.spadafora@futurenet.com (Anthony Spadafora) ]]></author>                    <dc:creator><![CDATA[ Anthony Spadafora ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/z73LEoj7FkUjNG85GcWHtH-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Anthony Spadafora is the managing editor for security and home office furniture at Tom’s Guide where he covers everything from data breaches and malware to password managers and the best way to cover your whole home or business with a strong Wi-Fi signal.&lt;/p&gt;&lt;p&gt;Before joining the team, he spent three years covering cybersecurity and B2B tech for ITProPortal while living in South Korea. After moving back to the US. Anthony joined the TechRadar Pro team where he covered these topics along with VPNs, web hosting, online collaboration software and video conferencing for four years. Anthony also has his ears to the ground and is on the lookout for the next major cyberattack or data breach.&lt;/p&gt;&lt;p&gt;Based in Houston, Texas, Anthony also handles VPN testing for both Tom’s Guide and TechRadar. As someone who has worked from home exclusively since 2018, he has reviewed dozens of standing desks as well as office chairs and has taken a closer look at other essential remote working accessories. As part of these reviews, Anthony frequently builds intricate desk setups which is why he’s such a big advocate for cable management and keeping things organized. When he’s not writing, he can be found tinkering with PCs and game consoles, managing cables and making upgrades to his smart home.&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/vMGpmubyydhZ3u2CbUbHMW-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The Keychron Q11 Ultra split mechanical keyboard next to a vertical mouse and a mini PC on a desk]]></media:description>                                                            <media:text><![CDATA[The Keychron Q11 Ultra split mechanical keyboard next to a vertical mouse and a mini PC on a desk]]></media:text>
                                <media:title type="plain"><![CDATA[The Keychron Q11 Ultra split mechanical keyboard next to a vertical mouse and a mini PC on a desk]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/vMGpmubyydhZ3u2CbUbHMW-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Just when I thought I’d never go back to using a <a href="https://www.tomsguide.com/computing/peripherals/i-finally-tried-magnetic-key-switches-but-i-never-thought-id-find-them-in-a-split-keyboard-thats-also-wireless">split mechanical keyboard</a>, Keychron’s new Q11 Ultra came along and fixed everything I disliked about my previous one while taking the typing experience to a whole new level.</p><p>Switching from a mushy membrane keyboard to one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> was one of the biggest upgrades I’ve ever made. Doing so not only improved my typing, but it also helped reduce hand fatigue. There was just one problem: ergonomics.</p><p>That’s where split mechanical keyboards come in. As the name suggests, these keyboards are made up of two halves that work together but can be spread apart on your desk. This way, your arms rest at a more natural position while typing, keeping them shoulder-width apart.</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/njtcgcqZWXk" allowfullscreen></iframe></div></div><p>For most people, using a standard mechanical keyboard will still be a massive improvement over a membrane one. Given that I write for a living and type a lot more than most people do each day, I wanted to take things to the next level.</p><p>Now, the Q11 Ultra isn’t the first split mechanical keyboard I’ve used. However, it has one big upgrade that I’ve been waiting to try out for myself. After using Keychron’s latest split keyboard for a week, I don’t know how I can go back to a standard mechanical keyboard.</p><div class="product"><a data-dimension112="a194218a-460d-460c-a495-2db6c240493b" data-action="Deal Block" data-label="Q11 Ultra" data-dimension48="Q11 Ultra" data-dimension25="$239" href="https://www.amazon.com/Keychron-Ergonomic-Mechanical-Open-Source-Hot-Swappable/dp/B0GQT4YC5H?th=1" 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:75.33%;"><img id="ELTWHiYVeNbjqoR7GP4zX9" name="Q11 Ultra" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ELTWHiYVeNbjqoR7GP4zX9-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1130" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>This 75% split mechanical keyboard features an incredibly premium full-metal chassis and an ultra-fast 8,000Hz wireless polling rate. Its completely tether-free design allows you to position the two halves wherever they feel most comfortable on your desk, while Keychron's hot-swappable Silk POM switches offer a self-lubricating, ultra-smooth typing experience that actively helps reduce hand fatigue. The Q11 Ultra also comes with extra switches, a keycap puller, and dual customizable control knobs.</p><p><strong>Price Check:</strong> <a href="https://www.keychron.com/products/keychron-q11-ultra-8k-wireless-custom-mechanical-keyboard" target="_blank" rel="nofollow" data-dimension112="a194218a-460d-460c-a495-2db6c240493b" data-action="Deal Block" data-label="Q11 Ultra" data-dimension48="Q11 Ultra" data-dimension25="$239">$239 @ Keychron</a><a class="view-deal button" href="https://www.amazon.com/Keychron-Ergonomic-Mechanical-Open-Source-Hot-Swappable/dp/B0GQT4YC5H?th=1" target="_blank" rel="nofollow" data-dimension112="a194218a-460d-460c-a495-2db6c240493b" data-action="Deal Block" data-label="Q11 Ultra" data-dimension48="Q11 Ultra" data-dimension25="$239">View Deal</a></p></div><h2 id="a-premium-keyboard-with-a-twist">A premium keyboard with a twist</h2><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="A7oRonmK7CJgubP62qZoWZ" name="Keychron Q11 Ultra-6" alt="The Keychron Q11 Ultra unboxed on a desk" src="https://cdn.mos.cms.futurecdn.net/A7oRonmK7CJgubP62qZoWZ-1920-80.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 Q11 Ultra comes with plenty of useful accessories in the box. From Windows-specific keycaps to extra mechanical switches and a USB-C to USB-A adapter, you have everything you need to get started and to tweak this keyboard to your liking.</p><p>Just like with the <a href="https://www.tomsguide.com/computing/keyboards/i-tried-the-new-keychron-q1-ultra-8k-and-this-hefty-full-featured-keyboard-is-a-game-changer-for-my-workflow">Keychron Q1 Ultra</a> I tried earlier this year, the Q11 Ultra is an 8K mechanical keyboard with a premium design and build. This higher polling rate is often associated with the <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> but there’s a benefit for fast typists too, as it results in a very responsive, fluid, and connected feeling.</p><p>In the past, you’d have to use the included cable to experience an 8000 Hz polling rate. This isn’t the case with the Q11 Ultra though, as you can do so wirelessly using the included 2.4 GHz USB dongle. This just so happens to be my preferred connection method too, since I’m always switching between computers while reviewing the <a href="https://www.tomsguide.com/us/best-mini-pc.html">best mini PCs</a>. Instead of dealing with Bluetooth pairing, I just plug in the dongle and I’m good to go.</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="yoUFwXnMRCi84ueiZGQAT3" name="Keychron Q11 Ultra-5" alt="The Keychron Q11 Ultra on a desk with both halves connected together" src="https://cdn.mos.cms.futurecdn.net/yoUFwXnMRCi84ueiZGQAT3-1920-80.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 Q11 Ultra features KSA double-shot PBT keycaps like on the Q1 Ultra. They look great and feel just as good underneath your fingertips. Besides being a split keyboard, the Q11 Ultra has some other unique features too. For instance, instead of just one knob on the top right side of the keyboard, there’s a second knob on the left side too. Out of the box, both are set to control your computer’s volume while pressing the knob down mutes your audio. However, in the web-based <a href="https://launcher.keychron.com/#/not-connect">Keychron Launcher</a>, you can customize them and the rest of the keyboard’s keys. Likewise, you’ll need to use it to activate the Q11 Ultra’s faster 8K polling rate too.</p><p>Although the Q11 Ultra is a 75% keyboard with a function row up top and arrow keys in the bottom right corner, it does have a few extra macro keys on the left side. Within the Keychron Launcher, you can program M1-M5 any way you want. For instance, I currently have M1 set to open a new File Explorer in Windows while I have M2 set to open Task View so I can easily switch between virtual desktops. The possibilities are basically endless since you can also record your own macros — or multi-key shortcuts — and assign them to any of these M keys 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="3uayeVdj5RvN3Mws6tuMd8" name="Keychron Q11 Ultra-4" alt="The backside of the Keychron Q11 Ultra showing off its power and input mode toggles along with its USB-C ports" src="https://cdn.mos.cms.futurecdn.net/3uayeVdj5RvN3Mws6tuMd8-1920-80.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>Now around back is where things get interesting with the Q11 Ultra. On the left half, you have a USB-C charging port and two toggle switches: one to switch between connection modes (Bluetooth, wired and 2.4 GHz) and another to switch between Mac or Windows. Then on the right half, you have another USB-C charging port for that side of the Q11 Ultra and a simple on/off switch. This switch can come in handy if you just want to play one of the <a href="https://www.tomsguide.com/best-picks/best-PC-games">best PC games</a> with just the left side of the keyboard to make more room for your mouse.</p><p>With the Q11 Ultra unboxed, it was time to bring it over to my desk and get it set up.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-O6jjGO"></div>                            </div>                            <script src="https://kwizly.com/embed/O6jjGO.js" async></script><h2 id="a-no-compromise-split">A no-compromise split</h2><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="iqEh2dGs8k8jrCR25BTRnC" name="Keychron Q11 Ultra-3" alt="A desk setup built around the Keychron Q11 Ultra and the Geekom A9 Max 2026 Edition" src="https://cdn.mos.cms.futurecdn.net/iqEh2dGs8k8jrCR25BTRnC-1920-80.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>Since I’m in the process of reviewing the 2026 Edition of the <a href="https://www.tomsguide.com/computing/desktop-computers/mini-pcs/geekom-a9-max-review">Geekom A9 Max</a>, I decided to build a whole productivity-focused desk setup around it with the Q11 Ultra front and center. On the right, instead of my trusty <a href="https://www.tomsguide.com/reference/what-is-a-trackball-mouse">trackball mouse</a>, I’ve paired this keyboard with the Keychron M5 vertical mouse I’m also testing out. Then in the center above the A9 Max 2026 Edition, I have the <a href="https://www.tomsguide.com/computing/monitors/i-ditched-my-dual-monitor-setup-for-a-single-40-inch-ultrawide-and-two-screens-arent-better-than-one">Innocn 40C1U</a> 40-inch ultrawide monitor for as much screen real estate as I can get without having to go from a flat to a curved display. And if you’re wondering about the desk, this is the <a href="https://www.tomsguide.com/home/home-office/uplift-v3-standing-desk-review">Uplift V3</a> standing desk with power grommets on either side.</p><p>Although the monitor and the mini PC powering this whole setup are excellent in their own right, it’s the Q11 Ultra that’s made the biggest difference. It feels just as good to type on as the Q1 Ultra since I’m also using Keychron’s own Silk POM Banana switches, which feature an early tactile bump compared to traditional brown mechanical switches. When combined with the KSA double-shot PBT keycaps and the Q11 Ultra’s full metal body, the whole experience of using this keyboard has an incredibly premium feel to it.</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="JHqG6SZYPiFZbaWhdoD9VG" name="Keychron Q11 Ultra-2" alt="A person typing on the Keychron Q11 Ultra split mechanical keyboard at their desk" src="https://cdn.mos.cms.futurecdn.net/JHqG6SZYPiFZbaWhdoD9VG-1920-80.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 best part about the Q11 Ultra is its tether-free design. In the past, the two halves of a split mechanical keyboard needed to be connected to one another using a short cable. With the Q11 Ultra, Keychron uses advanced 2.4 GHz wireless sync to allow the two halves to communicate seamlessly without one. What I like about this is that you can have the two halves of this split keyboard as far apart or as close to one another as you’d like. Although I prefer having the two halves aligned, you can also tilt them inward or outward depending on your preference.</p><p>Keychron has also made a lot of improvements to the acoustic profile of the Q11 Ultra compared to the original <a href="https://www.tomsguide.com/computing/peripherals/keychron-q11-review">Q11</a>. While you just had one layer of sound-absorbing foam with the latter, this upgraded model has two layers of foam between its switches and PCB, as well as two more at the very bottom. As such, the Q11 Ultra sounds just as good while typing as it feels underneath your fingertips.</p><p>Even though I’ve kept it off for the most part, you also have full RGB backlighting as well as per-key RGB, where you can individually customize the color of each key. Just like with 8K polling, leaving the Q11 Ultra’s RGB backlighting on will drain its battery faster. Speaking of battery life, Keychron says you can get up to 160 hours on a single charge. I’ve been using the Q11 Ultra as my daily driver for over a week now and I haven’t had to charge it yet.</p><div class="product"><a data-dimension112="13353d62-0bfc-43fc-89e8-3ca916ff0cd1" data-action="Deal Block" data-label="Split70" data-dimension48="Split70" data-dimension25="$99" href="https://www.amazon.com/EPOMAKER-Split70-Wireless-Ergonomic-Mechanical/dp/B0FPQX172M" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1479px;"><p class="vanilla-image-block" style="padding-top:45.57%;"><img id="Nvxjk6tqpBJWRhEEyfSyA6" name="Split70" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/Nvxjk6tqpBJWRhEEyfSyA6-1920-80.jpg" mos="" align="middle" fullscreen="" width="1479" height="674" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/EPOMAKER-Split70-Wireless-Ergonomic-Mechanical/dp/B0FPQX172M" target="_blank" rel="nofollow" data-dimension112="13353d62-0bfc-43fc-89e8-3ca916ff0cd1" data-action="Deal Block" data-label="Split70" data-dimension48="Split70" data-dimension25="$99">Split70: was $115 now $99</a></strong><br>This 70% split mechanical keyboard is a budget-friendly alternative that features flexible tri-mode connectivity (Bluetooth, 2.4GHz wireless, and wired), hot-swappable switches, and fully programmable keys via VIA software customization. It also features a gasket-mounted design, a programmable rotary knob, RGB backlighting and pre-lubed switches right out of the box.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-Split70-Wireless-Ergonomic-Mechanical/dp/B0FPQX172M" target="_blank" rel="nofollow" data-dimension112="13353d62-0bfc-43fc-89e8-3ca916ff0cd1" data-action="Deal Block" data-label="Split70" data-dimension48="Split70" data-dimension25="$99">View Deal</a></p></div><h2 id="tether-free-finally">Tether-free, finally</h2><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="5oHiKwxWuZ46SS6i9jqW2S" name="Seenda Upgraded Trackpad-5.jpg" alt="A picture of a desk setup with a trackpad on the left side of a split mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/5oHiKwxWuZ46SS6i9jqW2S-1920-80.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>Back when I had my first split mechanical keyboard — the <a href="https://www.tomsguide.com/opinion/i-added-a-mini-monitor-to-my-desk-setup-and-its-changed-how-i-work-heres-how">Mistel Barocco</a> — almost everything except for my mouse was connected to my PC via wires. Sure, it was dependable and lag-free, but as my <a href="https://www.tomsguide.com/computing/i-screenmaxxed-my-desk-setup-by-going-from-three-to-five-displays-but-they-arent-all-monitors">desk setups</a> have evolved, having wires running across the top of my desk just didn’t do all the work I put into my <a href="https://www.tomsguide.com/opinion/i-spent-a-year-reviewing-standing-desks-and-this-is-the-one-accessory-i-cant-live-without">under-desk cable management</a> justice.</p><p>After picking up the <a href="https://www.tomsguide.com/computing/peripherals/i-spent-just-dollar30-on-a-mechanical-keyboard-to-prove-you-dont-need-to-waste-your-money">Keychron C3 Pro</a> for just $30 on a whim a few years ago to see how far budget mechanical keyboards had come, I fell for Keychron, and I fell hard. While I initially gravitated toward its low-profile mechanical options, getting a chance to try the Q1 Ultra recently really made me appreciate its full-profile boards.</p><p>I almost picked up the original Q11 several times over the years, but in retrospect, I’m glad I waited for the Q11 Ultra. Keychron’s Banana switches feel like a massive improvement over traditional MX Browns, and its tether-free design is something I never thought I’d see at this price point. At $239, the Q11 Ultra is far from a starter board, but given what premium mechanical setups used to cost only a few years ago, it’s an absolute steal for this level of design and wireless functionality.</p><p>If you’ve already invested in one of the <a href="https://www.tomsguide.com/home/home-office/best-office-chairs">best office chairs</a> and a solid <a href="https://www.tomsguide.com/home/home-office/best-standing-desks">standing desk</a> to go alongside it, a split mechanical keyboard could be the last piece of the ergonomic puzzle you’ve been missing. I used to think a standard mechanical layout was good enough for me too, but after spending a week daily driving the Q11 Ultra, going back is going to be incredibly tough.</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/computing/keyboards/ive-used-the-same-usd30-logitech-keyboard-for-10-years-and-framework-just-showed-off-the-upgrade-i-actually-want">I've used the same $30 Logitech keyboard for 10 years and Framework just showed off the upgrade I actually want</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-tested-the-worlds-first-concrete-keyboard-theres-a-reason-nobody-else-does-this">I tested the world’s first concrete keyboard and there’s a reason nobody else does this</a></li><li><a href="https://www.tomsguide.com/computing/i-ditched-my-laptop-for-a-pocketable-mini-pc-and-a-pair-of-ar-glasses-heres-what-happened">I gave my favorite pocketable mini PC an instant upgrade with this docking station — and now it’s a full-fledged gaming rig</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I almost found my dream keyboard in the GravaStar Mercury V60 Pro, if it weren't for three finicky details ]]></title>
                                                                                                <dc:content><![CDATA[ <p>When I first ran my fingers across the GravaStar Mercury V60 Pro, I thought I'd found my next daily driver. As a sci-fi nerd, I got an instant kick out of its distinctive H.R. Giger-inspired styling, with its precision-cut aluminum upper frame giving me serious Xenomorph vibes. </p><p>Lifting out of the box, I was also struck by how weighty and substantial it felt — this is not a keyboard that's going to slide all over my desk, nor is it one that would be in danger of sounding hollow or plasticky. </p><p>What really grabbed me, though, was the feel and acoustics of the typing experience. Having experimented with many types of switches with my own hobby keyboard, and with various other models I've tested over the years, I was struck by how satisfying it was to type on the Mercury V60 Pro.</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="pZPF23UPuQvZ7HyXFZ9fLo" name="GravaStar Mercury V60 Pro-7" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/pZPF23UPuQvZ7HyXFZ9fLo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 don't mean to sound hyperbolic, but it is without question the most luxurious sounding keyboard I've ever typed on, achieving a remarkably deep and creamy "thock" with every keystroke — it's what I imagine typing on a keyboard made of chocolate would feel like.</p><p>I have to assume that part of this is due to GravaStar's factory-lubed UFO Hall-effect switches, which provide a smooth, frictionless glide. That said, I've used other <a href="https://www.tomsguide.com/computing/peripherals/i-finally-tried-magnetic-key-switches-but-i-never-thought-id-find-them-in-a-split-keyboard-thats-also-wireless">magnetic switches</a> in the past that didn't sound even half as thocky as they do on the Mercury V60 Pro.</p><p>While I've previously <a href="https://www.tomsguide.com/computing/keyboards/corsairs-vanguard-pro-96-keyboard-has-convinced-me-that-magnetic-switches-arent-just-hype">showered admiration</a> on the MGX Hyperdrive switches in <a href="https://www.tomsguide.com/gaming/gaming-peripherals/corsair-vanguard-pro-96-review">Corsair’s Vanguard Pro 96</a>, their sound profile is far brighter and more clacky by comparison. They also exhibit more wobble than the Mercury V60 Pro’s UFO switches, which stand out as some of the most solid and stable switches I’ve tested.</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="HtntPNGo89vaus8BjWXy6o" name="GravaStar Mercury V60 Pro-4" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/HtntPNGo89vaus8BjWXy6o-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 next contributing factor is the V60 Pro’s five layers of acoustic‑dampening foam, which do an impressive job of soaking up higher‑pitched sounds. On top of that, the keyboard’s pre‑installed metallic‑finish keycaps — a sturdy polycarbonate (PC) and Acrylonitrile Butadiene Styrene (ABS) blend — give each keystroke a deeper, more muted character compared to the semi‑translucent keycap set included in the deluxe 'Dual Keycap Gift Box Set'.</p><p>Of course, there's more to the Mercury V60 Pro than the way it sounds. As I mentioned earlier, it boasts ultra-responsive Hall-effect switches, with this model allowing you to adjust your actuation points by increments of 0.005mm (fully adjustable in GravaStar's browser-based software), starting at that number and reaching a maximum trigger depth up to 3.5mm.</p><p>It also supports dynamic Rapid Trigger, which tracks key travel continuously rather than relying on fixed reset points. This means keys can reset the moment they start to rise, allowing rapid‑fire repeats even when they’re still partially pressed — great news for those after flawless counter-strafing and jiggle-peeking in games like Counter-Strike 2 or Valorant.</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="fPBTwbbLA6Sg5NiQtPKsEo" name="GravaStar Mercury V60 Pro-5" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/fPBTwbbLA6Sg5NiQtPKsEo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, so what are my issues with the Mercury V60 Pro? For starters, its 60% form factor is just way too compact for me to type comfortably. Apart from its lack of certain keys, the need to constantly stop and think about how to access certain functions was slowing down my workflow. </p><p>My biggest size‑related gripe was the tiny right Shift key — It’s no larger than a standard key, and I kept missing it while typing, often wiping out an entire line in the process.</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="xUmpdBe5meGRVjQYrYfuAo" name="GravaStar Mercury V60 Pro-2" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/xUmpdBe5meGRVjQYrYfuAo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Secondly, I also really missed having a dedicated volume rocker or knob. Without one, I kept fumbling to remember which key combination controlled the volume every time I needed to raise or lower it.</p><p>And finally, I wasn't hugely impressed by the V60 Pro's RGB lighting effects. I definitely appreciate that the lights it has are south-facing — not that the metallic keycaps are shine-through or anything — though the LEDs don't emit a particularly strong light. I also expected that the RGBs would make the keyboard's included semi-translucent keycaps glow, though that wasn't the case.</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="FZcUVK64Qdr7X62vqittGo" name="GravaStar Mercury V60 Pro-6" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/FZcUVK64Qdr7X62vqittGo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 was really only these three small but persistent issues that held it back from being the complete package I'd hoped for. Granted, I could always try <a href="https://www.amazon.com.au/Mercury-V75-Pro-Competitive-Compatible/dp/B0G1MS9B7M/">GravaStar's Mercury V75 Pro keyboard</a>, which eliminates at least the first two complaints thanks to its larger size and included volume rocker, though its styling is far more aggressive than the V60 Pro, and without having one for reference, it's difficult to know if its typing acoustics are comparable.</p><p>Still, if these three nitpicks are unlikely to bother you, it would be hard to imagine anyone disappointed in the Mercury V60 Pro's capabilities. That said, a couple of folks around the Tom’s Guide office weren’t fans of the “creamy” sound profile that I love so much. Then again, maybe they just can’t stand the word <em>creamy</em> — hard to know which reaction was stronger.</p><div data-widget-type="productname" data-model-name="Gravastar Mercury V60 Pro"></div><div data-widget-type="productname" data-model-name="GravaStar Mercury V75 Pro HE"></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/computing/peripherals/mechanical-keyboards-are-dead-heres-why-you-should-only-buy-a-magnetic-keyboard-for-gaming">Here’s why you should only buy a magnetic keyboard for gaming</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/i-just-tested-my-favorite-hybrid-gaming-keyboard-of-the-year-and-i-cant-put-it-down">I just tested my favorite hybrid gaming keyboard of the year</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-he-review">I’m a mechanical switch die-hard — then I tested this magnetic keyboard...</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/i-almost-found-my-dream-keyboard-in-the-gravastar-mercury-v60-pro-if-it-werent-for-three-finicky-details</link>
                                                                            <description>
                            <![CDATA[ I'm absolutely in love with the acoustics and typing experience on this 60% keyboard, though a few small things hold it back from being perfect. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">fmfF3TGBJEPPsa86bytsGV</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/797tXBZXQxopmkS35GZp6S-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 22 May 2026 06:59:59 +0000</pubDate>                                                                                                                                <updated>Mon, 25 May 2026 01:15:41 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                                                                                    <dc:creator><![CDATA[ Stephen Lambrechts ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/JN3jEQpqyyt32as8AJJqXe-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Stephen Lambrechts is the Managing Editor of Tom&#039;s Guide AU and has written professionally across the categories of tech, film, television and gaming for the last 15 years. Before Tom&#039;s Guide, he spent several years as a Senior Journalist at TechRadar, had a brief stint as Editor in Chief at Official Xbox Magazine Australia, and worked at IGN. He&#039;s also written for such mags and sites as APC, TechLife Australia, T3, FilmInk, Daily Telegraph and AskMen. Additionally, he&#039;s a Tomatometer-approved critic on Rotten Tomatoes, and has offered his tech advice on radio and television.&lt;/p&gt;&lt;p&gt;He&#039;s an expert when it comes to smartphones, TVs, gaming and streaming, having contributed many reviews in each of those categories over the years. Despite his many articles on streaming, he&#039;s a passionate collector of physical media who enjoys listening to vinyl records and acquiring obscure horror movies from the &#039;60s, &#039;70s and 80s. He can usually be seen rocking the latest retro sneaker releases, particularly Air Jordans and Reebok Pumps. Occasionally, he also indulges in other non-hipster stuff, like hiking.&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/797tXBZXQxopmkS35GZp6S-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[GravaStar Mercury V60 Pro on a desk]]></media:description>                                                            <media:text><![CDATA[GravaStar Mercury V60 Pro on a desk]]></media:text>
                                <media:title type="plain"><![CDATA[GravaStar Mercury V60 Pro on a desk]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/797tXBZXQxopmkS35GZp6S-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>When I first ran my fingers across the GravaStar Mercury V60 Pro, I thought I'd found my next daily driver. As a sci-fi nerd, I got an instant kick out of its distinctive H.R. Giger-inspired styling, with its precision-cut aluminum upper frame giving me serious Xenomorph vibes. </p><p>Lifting out of the box, I was also struck by how weighty and substantial it felt — this is not a keyboard that's going to slide all over my desk, nor is it one that would be in danger of sounding hollow or plasticky. </p><p>What really grabbed me, though, was the feel and acoustics of the typing experience. Having experimented with many types of switches with my own hobby keyboard, and with various other models I've tested over the years, I was struck by how satisfying it was to type on the Mercury V60 Pro.</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="pZPF23UPuQvZ7HyXFZ9fLo" name="GravaStar Mercury V60 Pro-7" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/pZPF23UPuQvZ7HyXFZ9fLo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 don't mean to sound hyperbolic, but it is without question the most luxurious sounding keyboard I've ever typed on, achieving a remarkably deep and creamy "thock" with every keystroke — it's what I imagine typing on a keyboard made of chocolate would feel like.</p><p>I have to assume that part of this is due to GravaStar's factory-lubed UFO Hall-effect switches, which provide a smooth, frictionless glide. That said, I've used other <a href="https://www.tomsguide.com/computing/peripherals/i-finally-tried-magnetic-key-switches-but-i-never-thought-id-find-them-in-a-split-keyboard-thats-also-wireless">magnetic switches</a> in the past that didn't sound even half as thocky as they do on the Mercury V60 Pro.</p><p>While I've previously <a href="https://www.tomsguide.com/computing/keyboards/corsairs-vanguard-pro-96-keyboard-has-convinced-me-that-magnetic-switches-arent-just-hype">showered admiration</a> on the MGX Hyperdrive switches in <a href="https://www.tomsguide.com/gaming/gaming-peripherals/corsair-vanguard-pro-96-review">Corsair’s Vanguard Pro 96</a>, their sound profile is far brighter and more clacky by comparison. They also exhibit more wobble than the Mercury V60 Pro’s UFO switches, which stand out as some of the most solid and stable switches I’ve tested.</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="HtntPNGo89vaus8BjWXy6o" name="GravaStar Mercury V60 Pro-4" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/HtntPNGo89vaus8BjWXy6o-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 next contributing factor is the V60 Pro’s five layers of acoustic‑dampening foam, which do an impressive job of soaking up higher‑pitched sounds. On top of that, the keyboard’s pre‑installed metallic‑finish keycaps — a sturdy polycarbonate (PC) and Acrylonitrile Butadiene Styrene (ABS) blend — give each keystroke a deeper, more muted character compared to the semi‑translucent keycap set included in the deluxe 'Dual Keycap Gift Box Set'.</p><p>Of course, there's more to the Mercury V60 Pro than the way it sounds. As I mentioned earlier, it boasts ultra-responsive Hall-effect switches, with this model allowing you to adjust your actuation points by increments of 0.005mm (fully adjustable in GravaStar's browser-based software), starting at that number and reaching a maximum trigger depth up to 3.5mm.</p><p>It also supports dynamic Rapid Trigger, which tracks key travel continuously rather than relying on fixed reset points. This means keys can reset the moment they start to rise, allowing rapid‑fire repeats even when they’re still partially pressed — great news for those after flawless counter-strafing and jiggle-peeking in games like Counter-Strike 2 or Valorant.</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="fPBTwbbLA6Sg5NiQtPKsEo" name="GravaStar Mercury V60 Pro-5" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/fPBTwbbLA6Sg5NiQtPKsEo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, so what are my issues with the Mercury V60 Pro? For starters, its 60% form factor is just way too compact for me to type comfortably. Apart from its lack of certain keys, the need to constantly stop and think about how to access certain functions was slowing down my workflow. </p><p>My biggest size‑related gripe was the tiny right Shift key — It’s no larger than a standard key, and I kept missing it while typing, often wiping out an entire line in the process.</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="xUmpdBe5meGRVjQYrYfuAo" name="GravaStar Mercury V60 Pro-2" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/xUmpdBe5meGRVjQYrYfuAo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Secondly, I also really missed having a dedicated volume rocker or knob. Without one, I kept fumbling to remember which key combination controlled the volume every time I needed to raise or lower it.</p><p>And finally, I wasn't hugely impressed by the V60 Pro's RGB lighting effects. I definitely appreciate that the lights it has are south-facing — not that the metallic keycaps are shine-through or anything — though the LEDs don't emit a particularly strong light. I also expected that the RGBs would make the keyboard's included semi-translucent keycaps glow, though that wasn't the case.</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="FZcUVK64Qdr7X62vqittGo" name="GravaStar Mercury V60 Pro-6" alt="GravaStar Mercury V60 Pro on desk" src="https://cdn.mos.cms.futurecdn.net/FZcUVK64Qdr7X62vqittGo-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 was really only these three small but persistent issues that held it back from being the complete package I'd hoped for. Granted, I could always try <a href="https://www.amazon.com.au/Mercury-V75-Pro-Competitive-Compatible/dp/B0G1MS9B7M/">GravaStar's Mercury V75 Pro keyboard</a>, which eliminates at least the first two complaints thanks to its larger size and included volume rocker, though its styling is far more aggressive than the V60 Pro, and without having one for reference, it's difficult to know if its typing acoustics are comparable.</p><p>Still, if these three nitpicks are unlikely to bother you, it would be hard to imagine anyone disappointed in the Mercury V60 Pro's capabilities. That said, a couple of folks around the Tom’s Guide office weren’t fans of the “creamy” sound profile that I love so much. Then again, maybe they just can’t stand the word <em>creamy</em> — hard to know which reaction was stronger.</p><div data-widget-type="productname" data-model-name="Gravastar Mercury V60 Pro"></div><div data-widget-type="productname" data-model-name="GravaStar Mercury V75 Pro HE"></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/computing/peripherals/mechanical-keyboards-are-dead-heres-why-you-should-only-buy-a-magnetic-keyboard-for-gaming">Here’s why you should only buy a magnetic keyboard for gaming</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/i-just-tested-my-favorite-hybrid-gaming-keyboard-of-the-year-and-i-cant-put-it-down">I just tested my favorite hybrid gaming keyboard of the year</a></li><li><a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-he-review">I’m a mechanical switch die-hard — then I tested this magnetic keyboard...</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ This modern N64 controller might be the best Switch 2 pad for retro Nintendo games ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’ll be honest: one look at the 8BitDo 64 Controller and I was sold on it. Styled in the colors of a classic Nintendo N64 pad — albeit with the ergonomics brought up to modern day standards — the 8BitDo 64 is all about sheer mid-90s nostalgia.</p><p>The ergonomics aren’t the only update, though. The 64 uses USB-C and Bluetooth, making it compatible with PC, Switch, <a href="https://www.tomsguide.com/gaming/nintendo/nintendo-switch-2-console-review">Switch 2</a> and Android. Importantly, it’s also available in a 2.4G version that’s compatible with the original N64 (via a dongle). Meanwhile, both the Bluetooth and 2.4G versions work with the <a href="https://www.tomsguide.com/gaming/nintendo/analogue-3d-hands-on-review-nintendo-64">Analogue 3D</a> — a retro console that plays original N64 cartridges. So, yes: you can play Mario Kart 64 and GoldenEye with this thing.</p><p>If that wasn’t enough, there’s a Hall Effect joystick to eliminate stick drift, retro-feeling triggers, and a very reasonable asking price. Sure, this isn’t the <a href="https://www.tomsguide.com/best-picks/best-pc-game-controllers">best gaming controller</a> for esports, but it isn’t meant to be. It’s all about fun. And fun it is.</p><p>So, should you buy it? Keep reading my full 8BitDo 64 controller review to find out.</p><h2 class="article-body__section" id="section-8bitdo-64-review-cheat-sheet"><span>8BitDo 64 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A modern Bluetooth controller for N64 and classic Nintendo gaming</li><li><strong>What does it cost? </strong>It starts at <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B0DKX7JZQD" target="_blank" rel="nofollow">$39</a> / <a href="https://www.amazon.co.uk/Bluetooth-Controller-Analogue-Windows-Android/dp/B00LG0I9ZQ" target="_blank" rel="nofollow">£34</a>, so not much</li><li><strong>What’s good?</strong> The nostalgia, retro gaming performance, affordability, battery life, and compatibility options (including the original N64 and Analogue 3D)</li><li><strong>What’s not? </strong>The layout can be a bit awkward, and it isn’t suitable for modern gaming</li></ul><h2 class="article-body__section" id="section-8bitdo-64-review-specs"><span>8BitDo 64 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p>From <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B0DKX7JZQD" target="_blank" rel="nofollow">$39</a> / <a href="https://www.amazon.co.uk/Bluetooth-Controller-Analogue-Windows-Android/dp/B00LG0I9ZQ" target="_blank" rel="nofollow">£34</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.0; 2.4G; USB-C</p></td></tr><tr><td class="firstcol " ><p><strong>Compatibility</strong></p></td><td  ><p>PC, Android, Nintendo Switch, Nintendo Switch 2, N64, Analogue 3D</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>N64</p></td></tr><tr><td class="firstcol " ><p><strong>Triggers</strong></p></td><td  ><p>Mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Thumbstick</strong></p></td><td  ><p>Octagonal, Hall Effect</p></td></tr><tr><td class="firstcol " ><p><strong>Back buttons</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Rumble</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>5.54 x 4.09 x 2.38 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>7.9 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Color</strong></p></td><td  ><p>Black; White; Nintendo gray</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Up to 30 hours</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-8bitdo-64-review-the-ups"><span>8BitDo 64 review: The ups</span></h2><p>Ultimate retro styling (and feel), strong compatibility with appropriate devices, Hall Effect joysticks and a palatable price — what more could you want?</p><h2 id="sheer-mid-90s-nostalgia">Sheer mid-90s nostalgia...</h2><p>If you know anything about 8BitDo, you’ll know it’s no stranger to making some rather fetching gear — like the <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review">8BitDo Retro Mechanical Keyboard</a>, which our reviewer Nikita called “the best-looking keyboard” she’d ever tested. And I agree, having owned the dashing <a href="https://www.tomsguide.com/computing/keyboards/ive-used-this-retro-style-keyboard-for-a-year-and-its-still-my-favorite-even-if-it-drives-my-colleagues-mad">Commodore 64</a> version. </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="DJCS8756gdaJBxnv2YLDB" name="8BitDo_64_Bluetooth_Controller_012.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/DJCS8756gdaJBxnv2YLDB-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Well, the 64 Controller isn’t one to buck the trend. I find it hard to believe anyone would pass up having this thing on their shelf. </p><p>The 64 is available in a few different colorways. There are plain white and black versions — common to 8BitDo pads — which are great for minimalist setups or pairing with a white or black Analogue 3D. But it’s the N64 colorway that has my heart.</p><p>From the retro gray body and contrasting block color buttons, through to the right-hand-side button layout, and onto the damped triggers, this thing hits my nostalgia spot about as hard as is possible. On picking it up, I was immediately right back there: 8 years old, cross-legged on my best friend’s floor playing Super Mario 64 and GoldenEye on his big brother’s N64.</p><h2 id="with-a-modern-day-twist">…With a modern-day twist</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bmu98qPqWPAzTvrBQ8Hy5" name="8BitDo_64_Bluetooth_Controller_011.JPG" alt="The 8BitDo 64 Controller on a wooden surface" src="https://cdn.mos.cms.futurecdn.net/Bmu98qPqWPAzTvrBQ8Hy5-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>That said, while it looks the part, the 8BitDo 64 feels and acts like a modern gamepad. Its overall profile is essentially just that of the <a href="https://www.tomsguide.com/reviews/8bitdo-ultimate-controller">8BitDo Ultimate Controller</a>, which is a good thing.</p><p>The 64 is super comfortable in the hand, with chunky grips that make for a modern and more comfortable experience versus a triple-pronged original N64 pad (although it isn’t perfect, as I’ll cover later). It’s nice and lightweight too, at just 7.9 ounces, so it won’t be fatiguing to hold over long sessions.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/ZeQsoEj5X2oF3HZcgBa5YL-1920-80.jpg" alt="A screenshot of the 8BitDo 64 being updated/calibrated in the companion software." /><figcaption>As a modern controller, the 64 comes with companion software for updates and calibration of the Hall Effect joystick.<small role="credit">8BitDo</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/iDinmWnSYBHLAJDmMT4aXL-1920-80.jpg" alt="A screenshot of the 8BitDo 64 being updated/calibrated in the companion software." /><figcaption>As a modern controller, the 64 comes with companion software for updates and calibration of the Hall Effect joystick.<small role="credit">8BitDo</small></figcaption></figure></figure><p>Obviously, there are other modern, quality-of-life features, too. It’s wireless or wired, for a start, so no need to sit close to the screen like I did as a kid. You can pass the pad about from the luxury of your far-away sofa without the wire knocking over that glass of juice, or getting tangled with another pad. </p><p>The 64 also uses a Hall Effect joystick, to eliminate stick drift over time, giving you peace of mind about longevity. And you can update firmware using the companion software.</p><h2 id="appropriate-compatibility">Appropriate compatibility</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="r59e5Gh8i8Gz5SekrZkrQ" name="8BitDo_64_Bluetooth_Controller_019.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/r59e5Gh8i8Gz5SekrZkrQ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Huh? “Appropriate compatibility” — what the hell does that mean? It means exactly what it looks like. The 8BitDo 64 isn’t gonna work with your <a href="https://www.tomsguide.com/reviews/ps5">PS5</a> or Xbox, but why should it be? This is all about retro N64 gaming.</p><p>It <em>is</em> compatible, on the other hand, with the devices it <em>should</em> work with. The Nintendo Switch 2 and Switch, of course, as well as Android devices and the Analogue 3D console that runs N64 cartridges. There’s also PC as a catch-all for any folk who, like me, don’t game on any of the other supported devices (PCMR!). Importantly, if you buy the 2.4G version, you’ll lose compatibility with Switch and Android, but gain compatibility with the original N64 via a Wi-Fi dongle. Neat!</p><h2 id="awesome-for-retro-gaming">Awesome for retro gaming</h2><p>The 8BitDo 64 is awesome for retro gaming. Obviously, there’s the N64 control layout, giving you that classic D-pad / single stick feel to movement. It was so nostalgic to be playing in this format, and took a while to de-configure my brain and remember that A/B were closer to the middle, but I got there.</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="x5yAMBxbtLkU2B2yNAdHFo" name="8BitDo_64_Bluetooth_Controller_004.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/x5yAMBxbtLkU2B2yNAdHFo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 analog stick has an octagonal housing, allowing you to lock precisely into a direction — this is key to accuracy in older games not designed to cope with a true 360° stick. A nice touch is the addition of a metal collar around the stick stem, which stops the angled octagonal edges damaging the stem over time. As I mentioned above, the stick is Hall Effect, making it feel extremely quick and responsive.</p><p>I experienced no latency when gaming, even over Bluetooth, which kinda figures. The kind of games you’ll be playing with this pad aren’t the fastest-paced titles that demand the sharpest inputs. They’re older games with less precise controls. Still, it’s nice to know there’s the option for lower latency wired or 2.4G connectivity if needed.</p><h4 id="pc-doom-64">PC — Doom 64</h4><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Ef7nQaawTWFLMAwpWD9MG" name="8BitDo_64_Bluetooth_Controller_017.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/Ef7nQaawTWFLMAwpWD9MG-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 started my testing on <em>Doom 64</em>. As I’ll cover below, I found it a bit annoying having to stretch my right thumb over the controller to hit the D-pad for camera control. But other than that, I had a great time.</p><p>I especially enjoyed gunplay with the 8BitDo 64. The triggers are damped and kinda mushy-feeling compared to modern-vibe controllers, like my <a href="https://www.tomsguide.com/gaming/gaming-peripherals/razer-wolverine-v3-pro-review">Razer Wolverine V3 Pro</a> ($199) with its snappy hair triggers, and my <a href="https://www.tomsguide.com/gaming/gaming-peripherals/gamesir-kaleid-controller-review">Gamesir Kaleid</a> ($46) with its super smooth Hall Effect triggers. However, the 64 triggers have a nice thump to them, making each gunshot feel extremely satisfying — and again, this added to the retro 90s vibe.</p><h4 id="switch-2-mario-kart-goldeneye-star-fox">Switch 2 — Mario Kart, GoldenEye, Star Fox</h4><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="C43LppNHhQ5ymXyY7xdQBo" name="8BitDo_64_Bluetooth_Controller_005.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/C43LppNHhQ5ymXyY7xdQBo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 played some classic N64 games on the Switch 2. First, I played Mario Kart 64, which is the game I most enjoyed on the 8BitDo pad because it only requires the joystick for movement. I loved the octagonal groves here, which made it easy to lock in directions. Just like the OG N64 pad, the C buttons are right next to A and B, so you can quickly knock the C-down to launch powerups without taking your thumb off the gas.</p><p>I then played GoldenEye, which was even trickier to control than the OG game, as the Hall Effect stick is super sensitive — this made aiming with the reticule quite tricky. But I still had lots of fun on 007. I then finished off with some Star Fox, which was super easy to get up to speed with on the 8BitDo, and made good use of the 64’s Turbo function to cut down on thumb strain. </p><p>In all these games, Z (R2 on a modern pad) launched the game menu — with my brain programmed to modern gamepads where Z (R2) would be Fire, I had to do some reprogramming to stop hitting this and pausing my games. But you could just remap this in the controller button layout via the menu.</p><h2 id="decent-battery-life">Decent battery life</h2><p>The 8BitDo's rated battery life is (up to) 36 hours from its 1,000mAh cell. That’s a good amount of playtime, even if you’re gaming for 8 hour stints. It puts the 20 hours of my much pricier  Razer Wolverine V3 Pro to shame. </p><p>I haven't needed to charge the 8BitDo gamepad at all while testing, from around 8-12 hours of use (a large chunk of which was spent on Doom 64 — which I can't get enough of).</p><h2 id="affordable-price">Affordable price</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VUZxKuNBmGB9Gb7xV6Ls4" name="8BitDo_64_Bluetooth_Controller_010.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/VUZxKuNBmGB9Gb7xV6Ls4-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Best of all, the 8BitDo 64 is reasonably priced, at <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B0DKX7JZQD" target="_blank" rel="nofollow">$39 from Amazon for the black/white versions</a> and the <a href="https://www.amazon.com/8BitDo-Controller-Gaming-Console/dp/B0FF4XW3J8" target="_blank" rel="nofollow">2.4G gray version</a>, or <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B00LG0I9ZQ/" target="_blank" rel="nofollow">$44 for the Bluetooth version</a>. In the U.K., all versions cost <a href="https://www.amazon.co.uk/Bluetooth-Controller-Analogue-Windows-Android/dp/B00LG0I9ZQ" target="_blank" rel="nofollow">£34 at Amazon</a>. Given the decent build quality and how much fun this pad is for retro gaming, I’d say it’s a steal. And hey, even if you don’t use it that much, at least you won't have dropped big bucks.</p><h2 class="article-body__section" id="section-8bitdo-64-review-the-downs"><span>8BitDo 64 review: The downs</span></h2><p>The 8BitDo 64’s flaws are very few, and fairly minor. It isn’t the best for smaller hands, thanks to 8BitDo jerry-rigging the triple-pronged OG N64 layout into a conventional modern controller design. Plus it’s no good for non-retro gaming.</p><h2 id="won-t-be-good-for-smaller-hands">Won’t be good for smaller hands</h2><p>The original N64 pad wasn’t perfect, and seems very strangely laid out to modern sensibilities. Its layout served an important purpose at a time of emerging 3D gaming, and was the best Nintendo could do at the time… but it wasn’t perfect. You had to shift your hand between the prongs to play certain games. </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="N2VnNSBsaBRUpj323VwuPo" name="8BitDo_64_Bluetooth_Controller_007.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/N2VnNSBsaBRUpj323VwuPo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Don’t get me wrong, I love the N64 gamepad, it’s just a quirk of the controller, and symptomatic of where 3D gaming was at the time.</p><p>The 8BitDo 64, while a big improvement on the N64 pad’s layout in many games, is actually trickier to use than the OG N64 controller in games that use the D-Pad rather than C buttons for camera control. With the three-pronged N64 pad you’d shift your hand between the prongs, so one thumb was on the D-Pad and the other on the joystick.</p><p>The 8BitDo 64 has no middle prong and the D-Pad and joystick are next to one another. That means you need to stretch your hand across the pad to control the camera. In Doom 64, this was a bit annoying. I got by, as I have larger hands, but if you’ve got smaller mitts, you may find this problematic.</p><h2 id="no-good-for-non-retro-gaming">No good for non-retro gaming</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1622px;"><p class="vanilla-image-block" style="padding-top:66.58%;"><img id="p7r53DfhvLm8ddLm4tSGJ" name="8BitDo_64_Bluetooth_Controller_013.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/p7r53DfhvLm8ddLm4tSGJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1622" 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>This isn’t really a flaw, as it’s by design. However, with the octagonal joystick and no second stick, the 8BitDo 64 is no good for modern gaming. Unless you only play retro Nintendo games, the 64 is always going to be a supplemental purchase to your main pad — thankfully, it’s cheap enough to make that a non-issue.</p><p>If you instead need a single all-rounder pad, look at something like 8BitDo Ultimate or <a href="https://www.tomsguide.com/gaming/gaming-peripherals/gamesir-super-nova-review">GameSir Super Nova</a> ($49) instead. </p><h2 class="article-body__section" id="section-8bitdo-64-review-verdict"><span>8BitDo 64 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="KxG23LbDsJq2yZj6vj8zGo" name="8BitDo_64_Bluetooth_Controller_006.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/KxG23LbDsJq2yZj6vj8zGo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 love this little pad. It isn’t perfect, sure. It’s limited in its scope to retro gaming only, which will make this a secondary controller purchase for many, and if it was pricey, that would be a problem. But it isn’t. Meanwhile, jerry-rigging the inherently flawed N64 controls into a modern form has made the controls a little frustrating at times, but has dramatically improved the ergonomic experience to make up for it.</p><p>Those are really the only things I can say against the 8BitDo 64, and as you can see, my qualms were both heavily caveated. It’s a great supplementary pad to heighten the experience of playing retro games, by bringing the experience close to what was originally intended. It’s cute-looking, affordable, has decent battery life, and decent compatibility — especially with the N64 and Analogue 3D. What’s not to love?</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/gaming/gaming-peripherals/8bitdo-64-review</link>
                                                                            <description>
                            <![CDATA[ The 8BitDo 64 is a modern reincarnation of the original N64 gamepad, and I have very little to say against it. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">r8yZa7HzSrbdpRjSyD8tea</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/BonUXwJczNZ88dZqxLEAEo-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 21 May 2026 09:43:51 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:37 +0000</updated>
                                                                                                                                            <category><![CDATA[Gaming Peripherals]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Gaming]]></category>
                                                    <category><![CDATA[Computing]]></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-320-70.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/BonUXwJczNZ88dZqxLEAEo-1920-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The 8BitDo 64 Controller against a blue background]]></media:description>                                                            <media:text><![CDATA[The 8BitDo 64 Controller against a blue background]]></media:text>
                                <media:title type="plain"><![CDATA[The 8BitDo 64 Controller against a blue background]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/BonUXwJczNZ88dZqxLEAEo-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’ll be honest: one look at the 8BitDo 64 Controller and I was sold on it. Styled in the colors of a classic Nintendo N64 pad — albeit with the ergonomics brought up to modern day standards — the 8BitDo 64 is all about sheer mid-90s nostalgia.</p><p>The ergonomics aren’t the only update, though. The 64 uses USB-C and Bluetooth, making it compatible with PC, Switch, <a href="https://www.tomsguide.com/gaming/nintendo/nintendo-switch-2-console-review">Switch 2</a> and Android. Importantly, it’s also available in a 2.4G version that’s compatible with the original N64 (via a dongle). Meanwhile, both the Bluetooth and 2.4G versions work with the <a href="https://www.tomsguide.com/gaming/nintendo/analogue-3d-hands-on-review-nintendo-64">Analogue 3D</a> — a retro console that plays original N64 cartridges. So, yes: you can play Mario Kart 64 and GoldenEye with this thing.</p><p>If that wasn’t enough, there’s a Hall Effect joystick to eliminate stick drift, retro-feeling triggers, and a very reasonable asking price. Sure, this isn’t the <a href="https://www.tomsguide.com/best-picks/best-pc-game-controllers">best gaming controller</a> for esports, but it isn’t meant to be. It’s all about fun. And fun it is.</p><p>So, should you buy it? Keep reading my full 8BitDo 64 controller review to find out.</p><h2 class="article-body__section" id="section-8bitdo-64-review-cheat-sheet"><span>8BitDo 64 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A modern Bluetooth controller for N64 and classic Nintendo gaming</li><li><strong>What does it cost? </strong>It starts at <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B0DKX7JZQD" target="_blank" rel="nofollow">$39</a> / <a href="https://www.amazon.co.uk/Bluetooth-Controller-Analogue-Windows-Android/dp/B00LG0I9ZQ" target="_blank" rel="nofollow">£34</a>, so not much</li><li><strong>What’s good?</strong> The nostalgia, retro gaming performance, affordability, battery life, and compatibility options (including the original N64 and Analogue 3D)</li><li><strong>What’s not? </strong>The layout can be a bit awkward, and it isn’t suitable for modern gaming</li></ul><h2 class="article-body__section" id="section-8bitdo-64-review-specs"><span>8BitDo 64 review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p>From <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B0DKX7JZQD" target="_blank" rel="nofollow">$39</a> / <a href="https://www.amazon.co.uk/Bluetooth-Controller-Analogue-Windows-Android/dp/B00LG0I9ZQ" target="_blank" rel="nofollow">£34</a></p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.0; 2.4G; USB-C</p></td></tr><tr><td class="firstcol " ><p><strong>Compatibility</strong></p></td><td  ><p>PC, Android, Nintendo Switch, Nintendo Switch 2, N64, Analogue 3D</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>N64</p></td></tr><tr><td class="firstcol " ><p><strong>Triggers</strong></p></td><td  ><p>Mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Thumbstick</strong></p></td><td  ><p>Octagonal, Hall Effect</p></td></tr><tr><td class="firstcol " ><p><strong>Back buttons</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Rumble</strong></p></td><td  ><p>Yes</p></td></tr><tr><td class="firstcol " ><p><strong>Dimensions</strong></p></td><td  ><p>5.54 x 4.09 x 2.38 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>7.9 ounces</p></td></tr><tr><td class="firstcol " ><p><strong>Color</strong></p></td><td  ><p>Black; White; Nintendo gray</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Up to 30 hours</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-8bitdo-64-review-the-ups"><span>8BitDo 64 review: The ups</span></h2><p>Ultimate retro styling (and feel), strong compatibility with appropriate devices, Hall Effect joysticks and a palatable price — what more could you want?</p><h2 id="sheer-mid-90s-nostalgia">Sheer mid-90s nostalgia...</h2><p>If you know anything about 8BitDo, you’ll know it’s no stranger to making some rather fetching gear — like the <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review">8BitDo Retro Mechanical Keyboard</a>, which our reviewer Nikita called “the best-looking keyboard” she’d ever tested. And I agree, having owned the dashing <a href="https://www.tomsguide.com/computing/keyboards/ive-used-this-retro-style-keyboard-for-a-year-and-its-still-my-favorite-even-if-it-drives-my-colleagues-mad">Commodore 64</a> version. </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="DJCS8756gdaJBxnv2YLDB" name="8BitDo_64_Bluetooth_Controller_012.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/DJCS8756gdaJBxnv2YLDB-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Well, the 64 Controller isn’t one to buck the trend. I find it hard to believe anyone would pass up having this thing on their shelf. </p><p>The 64 is available in a few different colorways. There are plain white and black versions — common to 8BitDo pads — which are great for minimalist setups or pairing with a white or black Analogue 3D. But it’s the N64 colorway that has my heart.</p><p>From the retro gray body and contrasting block color buttons, through to the right-hand-side button layout, and onto the damped triggers, this thing hits my nostalgia spot about as hard as is possible. On picking it up, I was immediately right back there: 8 years old, cross-legged on my best friend’s floor playing Super Mario 64 and GoldenEye on his big brother’s N64.</p><h2 id="with-a-modern-day-twist">…With a modern-day twist</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Bmu98qPqWPAzTvrBQ8Hy5" name="8BitDo_64_Bluetooth_Controller_011.JPG" alt="The 8BitDo 64 Controller on a wooden surface" src="https://cdn.mos.cms.futurecdn.net/Bmu98qPqWPAzTvrBQ8Hy5-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>That said, while it looks the part, the 8BitDo 64 feels and acts like a modern gamepad. Its overall profile is essentially just that of the <a href="https://www.tomsguide.com/reviews/8bitdo-ultimate-controller">8BitDo Ultimate Controller</a>, which is a good thing.</p><p>The 64 is super comfortable in the hand, with chunky grips that make for a modern and more comfortable experience versus a triple-pronged original N64 pad (although it isn’t perfect, as I’ll cover later). It’s nice and lightweight too, at just 7.9 ounces, so it won’t be fatiguing to hold over long sessions.</p><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/ZeQsoEj5X2oF3HZcgBa5YL-1920-80.jpg" alt="A screenshot of the 8BitDo 64 being updated/calibrated in the companion software." /><figcaption>As a modern controller, the 64 comes with companion software for updates and calibration of the Hall Effect joystick.<small role="credit">8BitDo</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/iDinmWnSYBHLAJDmMT4aXL-1920-80.jpg" alt="A screenshot of the 8BitDo 64 being updated/calibrated in the companion software." /><figcaption>As a modern controller, the 64 comes with companion software for updates and calibration of the Hall Effect joystick.<small role="credit">8BitDo</small></figcaption></figure></figure><p>Obviously, there are other modern, quality-of-life features, too. It’s wireless or wired, for a start, so no need to sit close to the screen like I did as a kid. You can pass the pad about from the luxury of your far-away sofa without the wire knocking over that glass of juice, or getting tangled with another pad. </p><p>The 64 also uses a Hall Effect joystick, to eliminate stick drift over time, giving you peace of mind about longevity. And you can update firmware using the companion software.</p><h2 id="appropriate-compatibility">Appropriate compatibility</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="r59e5Gh8i8Gz5SekrZkrQ" name="8BitDo_64_Bluetooth_Controller_019.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/r59e5Gh8i8Gz5SekrZkrQ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Huh? “Appropriate compatibility” — what the hell does that mean? It means exactly what it looks like. The 8BitDo 64 isn’t gonna work with your <a href="https://www.tomsguide.com/reviews/ps5">PS5</a> or Xbox, but why should it be? This is all about retro N64 gaming.</p><p>It <em>is</em> compatible, on the other hand, with the devices it <em>should</em> work with. The Nintendo Switch 2 and Switch, of course, as well as Android devices and the Analogue 3D console that runs N64 cartridges. There’s also PC as a catch-all for any folk who, like me, don’t game on any of the other supported devices (PCMR!). Importantly, if you buy the 2.4G version, you’ll lose compatibility with Switch and Android, but gain compatibility with the original N64 via a Wi-Fi dongle. Neat!</p><h2 id="awesome-for-retro-gaming">Awesome for retro gaming</h2><p>The 8BitDo 64 is awesome for retro gaming. Obviously, there’s the N64 control layout, giving you that classic D-pad / single stick feel to movement. It was so nostalgic to be playing in this format, and took a while to de-configure my brain and remember that A/B were closer to the middle, but I got there.</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="x5yAMBxbtLkU2B2yNAdHFo" name="8BitDo_64_Bluetooth_Controller_004.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/x5yAMBxbtLkU2B2yNAdHFo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 analog stick has an octagonal housing, allowing you to lock precisely into a direction — this is key to accuracy in older games not designed to cope with a true 360° stick. A nice touch is the addition of a metal collar around the stick stem, which stops the angled octagonal edges damaging the stem over time. As I mentioned above, the stick is Hall Effect, making it feel extremely quick and responsive.</p><p>I experienced no latency when gaming, even over Bluetooth, which kinda figures. The kind of games you’ll be playing with this pad aren’t the fastest-paced titles that demand the sharpest inputs. They’re older games with less precise controls. Still, it’s nice to know there’s the option for lower latency wired or 2.4G connectivity if needed.</p><h4 id="pc-doom-64">PC — Doom 64</h4><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Ef7nQaawTWFLMAwpWD9MG" name="8BitDo_64_Bluetooth_Controller_017.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/Ef7nQaawTWFLMAwpWD9MG-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 started my testing on <em>Doom 64</em>. As I’ll cover below, I found it a bit annoying having to stretch my right thumb over the controller to hit the D-pad for camera control. But other than that, I had a great time.</p><p>I especially enjoyed gunplay with the 8BitDo 64. The triggers are damped and kinda mushy-feeling compared to modern-vibe controllers, like my <a href="https://www.tomsguide.com/gaming/gaming-peripherals/razer-wolverine-v3-pro-review">Razer Wolverine V3 Pro</a> ($199) with its snappy hair triggers, and my <a href="https://www.tomsguide.com/gaming/gaming-peripherals/gamesir-kaleid-controller-review">Gamesir Kaleid</a> ($46) with its super smooth Hall Effect triggers. However, the 64 triggers have a nice thump to them, making each gunshot feel extremely satisfying — and again, this added to the retro 90s vibe.</p><h4 id="switch-2-mario-kart-goldeneye-star-fox">Switch 2 — Mario Kart, GoldenEye, Star Fox</h4><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="C43LppNHhQ5ymXyY7xdQBo" name="8BitDo_64_Bluetooth_Controller_005.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/C43LppNHhQ5ymXyY7xdQBo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 played some classic N64 games on the Switch 2. First, I played Mario Kart 64, which is the game I most enjoyed on the 8BitDo pad because it only requires the joystick for movement. I loved the octagonal groves here, which made it easy to lock in directions. Just like the OG N64 pad, the C buttons are right next to A and B, so you can quickly knock the C-down to launch powerups without taking your thumb off the gas.</p><p>I then played GoldenEye, which was even trickier to control than the OG game, as the Hall Effect stick is super sensitive — this made aiming with the reticule quite tricky. But I still had lots of fun on 007. I then finished off with some Star Fox, which was super easy to get up to speed with on the 8BitDo, and made good use of the 64’s Turbo function to cut down on thumb strain. </p><p>In all these games, Z (R2 on a modern pad) launched the game menu — with my brain programmed to modern gamepads where Z (R2) would be Fire, I had to do some reprogramming to stop hitting this and pausing my games. But you could just remap this in the controller button layout via the menu.</p><h2 id="decent-battery-life">Decent battery life</h2><p>The 8BitDo's rated battery life is (up to) 36 hours from its 1,000mAh cell. That’s a good amount of playtime, even if you’re gaming for 8 hour stints. It puts the 20 hours of my much pricier  Razer Wolverine V3 Pro to shame. </p><p>I haven't needed to charge the 8BitDo gamepad at all while testing, from around 8-12 hours of use (a large chunk of which was spent on Doom 64 — which I can't get enough of).</p><h2 id="affordable-price">Affordable price</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VUZxKuNBmGB9Gb7xV6Ls4" name="8BitDo_64_Bluetooth_Controller_010.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/VUZxKuNBmGB9Gb7xV6Ls4-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Best of all, the 8BitDo 64 is reasonably priced, at <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B0DKX7JZQD" target="_blank" rel="nofollow">$39 from Amazon for the black/white versions</a> and the <a href="https://www.amazon.com/8BitDo-Controller-Gaming-Console/dp/B0FF4XW3J8" target="_blank" rel="nofollow">2.4G gray version</a>, or <a href="https://www.amazon.com/Bluetooth-Controller-Analogue-Windows-Gaming-Console/dp/B00LG0I9ZQ/" target="_blank" rel="nofollow">$44 for the Bluetooth version</a>. In the U.K., all versions cost <a href="https://www.amazon.co.uk/Bluetooth-Controller-Analogue-Windows-Android/dp/B00LG0I9ZQ" target="_blank" rel="nofollow">£34 at Amazon</a>. Given the decent build quality and how much fun this pad is for retro gaming, I’d say it’s a steal. And hey, even if you don’t use it that much, at least you won't have dropped big bucks.</p><h2 class="article-body__section" id="section-8bitdo-64-review-the-downs"><span>8BitDo 64 review: The downs</span></h2><p>The 8BitDo 64’s flaws are very few, and fairly minor. It isn’t the best for smaller hands, thanks to 8BitDo jerry-rigging the triple-pronged OG N64 layout into a conventional modern controller design. Plus it’s no good for non-retro gaming.</p><h2 id="won-t-be-good-for-smaller-hands">Won’t be good for smaller hands</h2><p>The original N64 pad wasn’t perfect, and seems very strangely laid out to modern sensibilities. Its layout served an important purpose at a time of emerging 3D gaming, and was the best Nintendo could do at the time… but it wasn’t perfect. You had to shift your hand between the prongs to play certain games. </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="N2VnNSBsaBRUpj323VwuPo" name="8BitDo_64_Bluetooth_Controller_007.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/N2VnNSBsaBRUpj323VwuPo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Don’t get me wrong, I love the N64 gamepad, it’s just a quirk of the controller, and symptomatic of where 3D gaming was at the time.</p><p>The 8BitDo 64, while a big improvement on the N64 pad’s layout in many games, is actually trickier to use than the OG N64 controller in games that use the D-Pad rather than C buttons for camera control. With the three-pronged N64 pad you’d shift your hand between the prongs, so one thumb was on the D-Pad and the other on the joystick.</p><p>The 8BitDo 64 has no middle prong and the D-Pad and joystick are next to one another. That means you need to stretch your hand across the pad to control the camera. In Doom 64, this was a bit annoying. I got by, as I have larger hands, but if you’ve got smaller mitts, you may find this problematic.</p><h2 id="no-good-for-non-retro-gaming">No good for non-retro gaming</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1622px;"><p class="vanilla-image-block" style="padding-top:66.58%;"><img id="p7r53DfhvLm8ddLm4tSGJ" name="8BitDo_64_Bluetooth_Controller_013.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/p7r53DfhvLm8ddLm4tSGJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1622" 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>This isn’t really a flaw, as it’s by design. However, with the octagonal joystick and no second stick, the 8BitDo 64 is no good for modern gaming. Unless you only play retro Nintendo games, the 64 is always going to be a supplemental purchase to your main pad — thankfully, it’s cheap enough to make that a non-issue.</p><p>If you instead need a single all-rounder pad, look at something like 8BitDo Ultimate or <a href="https://www.tomsguide.com/gaming/gaming-peripherals/gamesir-super-nova-review">GameSir Super Nova</a> ($49) instead. </p><h2 class="article-body__section" id="section-8bitdo-64-review-verdict"><span>8BitDo 64 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="KxG23LbDsJq2yZj6vj8zGo" name="8BitDo_64_Bluetooth_Controller_006.JPG" alt="The 8BitDo 64 Controller against a blue background" src="https://cdn.mos.cms.futurecdn.net/KxG23LbDsJq2yZj6vj8zGo-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 love this little pad. It isn’t perfect, sure. It’s limited in its scope to retro gaming only, which will make this a secondary controller purchase for many, and if it was pricey, that would be a problem. But it isn’t. Meanwhile, jerry-rigging the inherently flawed N64 controls into a modern form has made the controls a little frustrating at times, but has dramatically improved the ergonomic experience to make up for it.</p><p>Those are really the only things I can say against the 8BitDo 64, and as you can see, my qualms were both heavily caveated. It’s a great supplementary pad to heighten the experience of playing retro games, by bringing the experience close to what was originally intended. It’s cute-looking, affordable, has decent battery life, and decent compatibility — especially with the N64 and Analogue 3D. What’s not to love?</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The Cherry XTRFY M64 Wireless is a mighty comfortable mouse but it’s let down by glaring issues — and you can get more bang for your buck elsewhere ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a> come in all shapes and sizes: honeycomb, standard, you name it. The Cherry XTRFY M64 Wireless hopes to join the big leagues but just falls short. It’s a lovely-looking mouse, sporting a minimalist design, and it’s mighty comfortable too. With 26,000 DPI and 1,000Hz polling to boot, it boasts great gaming performance too.</p><p>So what’s the problem? The M64 Wireless isn’t without its flaws, and given its price, those flaws feel jarring. Surprisingly, there’s no companion software for customizing the mouse, and the lack of Bluetooth may be disappointing for some. The mouse also features an awkwardly-placed USB-C port that limits wired use.</p><p>For the complete breakdown, read my full Cherry XTRFY M64 Wireless review.</p><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-specs"><span>Cherry XTRFY M64 Wireless 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/CHERRY-XTRFY-Wireless-Ergonomic-Gaming/dp/B0DB2X74LD/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/CHERRY-XTRFY-Ultralight-Optimised-Ergonomic/dp/B0D4548YVZ/" target="_blank" rel="nofollow">£84</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>26,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>5</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.72 x 2.67 x 1.53 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.87oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White, Blue</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable Lithium-ion</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-cheat-sheet"><span>Cherry XTRFY M64 Wireless review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A right-handed mouse with 26,000 DPI and 1,000Hz polling</li><li><strong>Who is it for? </strong>Casual gamers</li><li><strong>How much does it cost? </strong>The Cherry XTRFY M64 Wireless is available for <a href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Ergonomic-Gaming/dp/B0DB2X74LD/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/CHERRY-XTRFY-Ultralight-Optimised-Ergonomic/dp/B0D4548YVZ/" target="_blank" rel="nofollow">£84</a></li><li><strong>What do we like? </strong>The low-key design, comfort level, variable 26,000 DPI, and great gaming performance</li><li><strong>What don’t we like? </strong>The lack of Bluetooth and companion software, and the awkwardly-placed USB-C port</li></ul><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-the-ups"><span>Cherry XTRFY M64 Wireless review: The ups</span></h2><p>The Cherry XTRFY M64 Wireless sports a lovely, minimalist and comfortable design, and its 26,000 DPI and 1,000Hz polling make it great for gaming.</p><h2 id="simplistic-low-key-design">Simplistic, low-key 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="asW5PtpzT2aPtvPmZ3qVTJ" name="Cherry_M64_mouse.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/asW5PtpzT2aPtvPmZ3qVTJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 be honest: I’m not usually a fan of flashy mice that feature loud RGB lighting. I also don’t like honeycomb mice as the design is divisive and I don’t like being able to see dust collect on the insides. Case in point: the <a href="https://www.tomsguide.com/computing/mice/steelseries-aerox-3-wireless-gen-2-review">SteelSeries Aerox 3 Wireless Gen 2</a> ($109). The Cherry XTRFY M64 Wireless, then, is the antithesis of the Aerox 3 Wireless Gen 2.</p><p>The M64 Wireless sports a simplistic, low-key design which makes it fit right into the office as well as most gaming setups. It’s available in Black, White and Blue colors. I tested the Black model with teal accents, and I love the look. The teal adds a nice pop of color without drawing too much attention to the mouse, and the teal strip under the scroll wheel looks lovely.</p><h2 id="comfortable-and-ergonomic">Comfortable and ergonomic</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZJNoZUjJ5PAGCCRs6XnyaJ" name="Cherry_M64_mouse 3.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/ZJNoZUjJ5PAGCCRs6XnyaJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Cherry XTRFY M64 Wireless is a very comfortable and ergonomic mouse. It’s designed for right-handed folks (sorry to my left-handed brethren), and it fits my hand like a glove. The mouse measures 4.72 x 2.67 x 1.53 inches so it’s well-suited for small- and medium-sized hands. My medium-sized hand had no issues cradling the M64 Wireless, and I found the matte finish provided a good grip on it 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="izEAiSczyjNFWhPJ4jXUMJ" name="Cherry_M64_mouse 6.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/izEAiSczyjNFWhPJ4jXUMJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 quite a light mouse too, weighing just 1.87oz. It’s lighter than the 2.39oz SteelSeries Aerox 3 Wireless Gen 2, the 2.75oz <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69), and the 2.32oz <a href="https://www.tomsguide.com/computing/peripherals/glorious-model-d2-gaming-mouse-review">Glorious Model D2</a> ($99). Though it’s light as a feather, the rubber strips on the M64 Wireless’ underside provide a sturdy grip on most surfaces. They also help it glide easily and without any friction.</p><p>Having used the M64 Wireless for the better part of a week, I can confidently say that it’s extremely comfortable. My hand and wrist didn’t feel fatigued even after using the mouse for a long gaming session.</p><h2 id="great-gaming-performance">Great gaming 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="ssHBQ2mBRJjz6YHtjPQzgJ" name="Cherry_M64_mouse 2.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/ssHBQ2mBRJjz6YHtjPQzgJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>What good is a comfortable mouse that isn’t great for gaming? Thankfully, the Cherry XTRFY M64 Wireless packs performance in spades, and it’s well-suited for casual gaming. It boasts a maximum DPI of 26,000 which is adjustable in four levels via the button on the mouse’s underside. You also get 1,000Hz polling over wired and 2.4GHz wireless connections.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>To test the M64 Wireless, I played <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> and Counter-Strike 2 on our testing rig. Being able to adjust the DPI on the fly was very handy as I could drop it down to 400 for slow-mo movements, or increase it to 26,000 for ultra-fast movements. When I was in close-quarters combat in either game, I opted for a high DPI as I could then move and attack quickly, especially when surrounded by multiple enemies at once. This enabled me to change directions speedily and attack NPCs with, say, my knife — or whatever weapon I had equipped.</p><p>But when it came to using a big weapon like a sniper rifle to take out targets at a distance, I dropped the DPI by pressing the designated button. This helped me slow down my movements and I could aim better. When there multiple NPCs standing together and I needed to take out just one target, I was able to fine-tune my movements and execute a precise headshot, move to the next enemy, rinse and repeat. They never saw me coming.</p><p>In addition to variable DPI, the M64 Wireless packs a 1,000Hz polling rate too, which negates latency issues and input lag. 1,000Hz polling is enough for casual players, but if you’re a competitive gamer who partakes in high-refresh-rate gaming, you may want to consider a 4,000Hz mouse like the SteelSeries Aerox 3 Wireless Gen 2, or an 8,000Hz mouse like the Keychron M6 8K.</p><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-the-downs"><span>Cherry XTRFY M64 Wireless review: The downs</span></h2><p>Unfortunately, the Cherry XTRFY M64 Wireless doesn’t feature Bluetooth or companion software, and the awkwardly-placed USB-C port makes it difficult to use the mouse wired.</p><h2 id="no-bluetooth">No Bluetooth</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="N8CEYFiHH5tX5Yi22NizcJ" name="Cherry_M64_mouse 7.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/N8CEYFiHH5tX5Yi22NizcJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Surprisingly, the Cherry XTRFY M64 Wireless doesn’t feature Bluetooth. Now, I’ve used and tested my fair share of mice, and this is probably the first time I’ve seen a wireless mouse not pack in Bluetooth connectivity. Cherry advertises this as a competitive gaming mouse so I can kinda see why there’s no Bluetooth. Polling over Bluetooth is, more often than not, limited to 125Hz, so realistically, no gamer would be using it.</p><p>But does that justify its absence? Not entirely. We’ve seen similarly priced mice, like the SteelSeries Aerox 3 Wireless Gen 2, feature Bluetooth connectivity. It’s nice for those who work or play across systems. The only way to connect the M64 Wireless wirelessly is by using the included 2.4GHz dongle… but there’s no slot on the mouse’s body to slot the dongle into, so be careful you don’t lose it!</p><h2 id="awkwardly-placed-port">Awkwardly-placed port</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QrFyxctgUbKsAL5S4QsegJ" name="Cherry_M64_mouse 4.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/QrFyxctgUbKsAL5S4QsegJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 wireless connectivity, the Cherry XTRFY M64 Wireless can be used wired too, thanks to the included USB-C cable. But what’s frustrating is the USB-C port’s location on the mouse’s body. For some reason, Cherry decided to put it alongside the right-hand edge of the mouse.</p><p>To say it’s awkward and makes the mouse practically unusable as your hand keeps pressing against it would be an understatement. What gives, Cherry? I suppose the silver lining here is that Cherry didn’t decide to place it on the mouse’s underside (looking at you, Apple Magic Mouse).</p><p>If you want a great mouse with port placement that makes sense, I’d recommend the SteelSeries Aerox 3 Wireless Gen 2, the Keychron M6 8K or the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m7-review">Keychron M7</a> ($49). All are excellent alternatives and they feature tri-mode connectivity.</p><h2 id="no-companion-software">No companion software</h2><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="ZJNoZUjJ5PAGCCRs6XnyaJ" name="Cherry_M64_mouse 3.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:241,l:148,cw:960,ch:540,q:80/ZJNoZUjJ5PAGCCRs6XnyaJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 thing that surprises me about the Cherry XTRFY M64 Wireless is its lack of companion software. Sure, you can change the DPI, debounce time and polling rate via the buttons on the top and the mouse’s underside, but to not have <em>any </em>form of companion software for further customization is disappointing. </p><p>I’d have liked to have been able to assign custom values to the DPI and polling rate buttons, instead of the set of four (each) preloaded ones.</p><h2 id="expensive-for-what-s-on-offer">Expensive for what’s on offer</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="N6bqKGqizXDNeAg5VN6KHJ" name="Cherry_M64_mouse 5.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/N6bqKGqizXDNeAg5VN6KHJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the Cherry XTRFY M64 Wireless is an expensive mouse. Its pros simply fail to outweigh the cons. The lack of companion software and Bluetooth, as well as an awkwardly placed port make it difficult to justify the M64 Wireless’ premium price. It retails for <a href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Ergonomic-Gaming/dp/B0DB2X74LD/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/CHERRY-XTRFY-Ultralight-Optimised-Ergonomic/dp/B0D4548YVZ/" target="_blank" rel="nofollow">£84 at Amazon</a> — that’s quite a bit!</p><p>Thing is, mice have come leaps and bounds in recent years, and you’ve got brands like Keychron making fantastic mice for less than $70 — case in point, the Keychron M6 8K and the Keychron M7. The M6 8K even offers 8,000Hz polling while the M64 Wireless tops out at 1,000Hz. If the M64 Wireless was a little cheaper, it would have been a different story. But as things stand, I find it difficult to recommend it outright when it faces such fierce competition.</p><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-verdict"><span>Cherry XTRFY M64 Wireless 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="2777B56QsMBacKYjqqzSLJ" name="Cherry_M64_mouse 1.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/2777B56QsMBacKYjqqzSLJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Cherry XTRFY M64 Wireless isn’t without its compromises. There’s no Bluetooth so wireless connectivity is limited to 2.4GHz. The lack of companion software means you can’t customize the buttons and their respective functions. And don’t get me started on the placement of the USB-C port.</p><p>But this doesn’t mean that the M64 Wireless is <em>all </em>bad. For the price, you get a comfortable and minimalistically designed mouse that’s a joy to use. 26,000 DPI and 1,000Hz polling also make it great for casual gaming.</p><p>Unfortunately, those strengths just don’t make up for the M64 Wireless’ weaknesses, and when the mouse faces such fierce competition from the likes of Keychron, it’s difficult to recommend it at its retail price.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/mice/cherry-xtrfy-m64-wireless-review</link>
                                                                            <description>
                            <![CDATA[ The Cherry XTRFY M64 Wireless boasts great gaming performance and 26,000 DPI, but it has glaring flaws that don't make it worth the asking price. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">H6Tc476SBVQkdh2DvQSMmD</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/D4CoHaCkXbFPPuCoXo3CpJ-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 18 May 2026 11:00:00 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:36 +0000</updated>
                                                                                                                                            <category><![CDATA[Mice]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/D4CoHaCkXbFPPuCoXo3CpJ-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black and teal Cherry XTRFY M64 Wireless gaming mouse]]></media:description>                                                            <media:text><![CDATA[A black and teal Cherry XTRFY M64 Wireless gaming mouse]]></media:text>
                                <media:title type="plain"><![CDATA[A black and teal Cherry XTRFY M64 Wireless gaming mouse]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/D4CoHaCkXbFPPuCoXo3CpJ-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The <a href="https://www.tomsguide.com/best-picks/best-gaming-mouse">best gaming mice</a> come in all shapes and sizes: honeycomb, standard, you name it. The Cherry XTRFY M64 Wireless hopes to join the big leagues but just falls short. It’s a lovely-looking mouse, sporting a minimalist design, and it’s mighty comfortable too. With 26,000 DPI and 1,000Hz polling to boot, it boasts great gaming performance too.</p><p>So what’s the problem? The M64 Wireless isn’t without its flaws, and given its price, those flaws feel jarring. Surprisingly, there’s no companion software for customizing the mouse, and the lack of Bluetooth may be disappointing for some. The mouse also features an awkwardly-placed USB-C port that limits wired use.</p><p>For the complete breakdown, read my full Cherry XTRFY M64 Wireless review.</p><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-specs"><span>Cherry XTRFY M64 Wireless 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/CHERRY-XTRFY-Wireless-Ergonomic-Gaming/dp/B0DB2X74LD/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/CHERRY-XTRFY-Ultralight-Optimised-Ergonomic/dp/B0D4548YVZ/" target="_blank" rel="nofollow">£84</a></p></td></tr><tr><td class="firstcol " ><p><strong>Max DPI</strong></p></td><td  ><p>26,000</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Buttons</strong></p></td><td  ><p>5</p></td></tr><tr><td class="firstcol " ><p><strong>Size</strong></p></td><td  ><p>4.72 x 2.67 x 1.53 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.87oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Black, White, Blue</p></td></tr><tr><td class="firstcol " ><p><strong>Body material</strong></p></td><td  ><p>Plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Design</strong></p></td><td  ><p>Right-handed</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable Lithium-ion</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated)</strong></p></td><td  ><p>Not specified</p></td></tr><tr><td class="firstcol " ><p><strong>Lighting</strong></p></td><td  ><p>No</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-cheat-sheet"><span>Cherry XTRFY M64 Wireless review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A right-handed mouse with 26,000 DPI and 1,000Hz polling</li><li><strong>Who is it for? </strong>Casual gamers</li><li><strong>How much does it cost? </strong>The Cherry XTRFY M64 Wireless is available for <a href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Ergonomic-Gaming/dp/B0DB2X74LD/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/CHERRY-XTRFY-Ultralight-Optimised-Ergonomic/dp/B0D4548YVZ/" target="_blank" rel="nofollow">£84</a></li><li><strong>What do we like? </strong>The low-key design, comfort level, variable 26,000 DPI, and great gaming performance</li><li><strong>What don’t we like? </strong>The lack of Bluetooth and companion software, and the awkwardly-placed USB-C port</li></ul><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-the-ups"><span>Cherry XTRFY M64 Wireless review: The ups</span></h2><p>The Cherry XTRFY M64 Wireless sports a lovely, minimalist and comfortable design, and its 26,000 DPI and 1,000Hz polling make it great for gaming.</p><h2 id="simplistic-low-key-design">Simplistic, low-key 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="asW5PtpzT2aPtvPmZ3qVTJ" name="Cherry_M64_mouse.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/asW5PtpzT2aPtvPmZ3qVTJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 be honest: I’m not usually a fan of flashy mice that feature loud RGB lighting. I also don’t like honeycomb mice as the design is divisive and I don’t like being able to see dust collect on the insides. Case in point: the <a href="https://www.tomsguide.com/computing/mice/steelseries-aerox-3-wireless-gen-2-review">SteelSeries Aerox 3 Wireless Gen 2</a> ($109). The Cherry XTRFY M64 Wireless, then, is the antithesis of the Aerox 3 Wireless Gen 2.</p><p>The M64 Wireless sports a simplistic, low-key design which makes it fit right into the office as well as most gaming setups. It’s available in Black, White and Blue colors. I tested the Black model with teal accents, and I love the look. The teal adds a nice pop of color without drawing too much attention to the mouse, and the teal strip under the scroll wheel looks lovely.</p><h2 id="comfortable-and-ergonomic">Comfortable and ergonomic</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZJNoZUjJ5PAGCCRs6XnyaJ" name="Cherry_M64_mouse 3.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/ZJNoZUjJ5PAGCCRs6XnyaJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Cherry XTRFY M64 Wireless is a very comfortable and ergonomic mouse. It’s designed for right-handed folks (sorry to my left-handed brethren), and it fits my hand like a glove. The mouse measures 4.72 x 2.67 x 1.53 inches so it’s well-suited for small- and medium-sized hands. My medium-sized hand had no issues cradling the M64 Wireless, and I found the matte finish provided a good grip on it 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="izEAiSczyjNFWhPJ4jXUMJ" name="Cherry_M64_mouse 6.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/izEAiSczyjNFWhPJ4jXUMJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 quite a light mouse too, weighing just 1.87oz. It’s lighter than the 2.39oz SteelSeries Aerox 3 Wireless Gen 2, the 2.75oz <a href="https://www.tomsguide.com/computing/mice/keychron-m6-8k-review">Keychron M6 8K</a> ($69), and the 2.32oz <a href="https://www.tomsguide.com/computing/peripherals/glorious-model-d2-gaming-mouse-review">Glorious Model D2</a> ($99). Though it’s light as a feather, the rubber strips on the M64 Wireless’ underside provide a sturdy grip on most surfaces. They also help it glide easily and without any friction.</p><p>Having used the M64 Wireless for the better part of a week, I can confidently say that it’s extremely comfortable. My hand and wrist didn’t feel fatigued even after using the mouse for a long gaming session.</p><h2 id="great-gaming-performance">Great gaming 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="ssHBQ2mBRJjz6YHtjPQzgJ" name="Cherry_M64_mouse 2.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/ssHBQ2mBRJjz6YHtjPQzgJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>What good is a comfortable mouse that isn’t great for gaming? Thankfully, the Cherry XTRFY M64 Wireless packs performance in spades, and it’s well-suited for casual gaming. It boasts a maximum DPI of 26,000 which is adjustable in four levels via the button on the mouse’s underside. You also get 1,000Hz polling over wired and 2.4GHz wireless connections.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>To test the M64 Wireless, I played <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a> and Counter-Strike 2 on our testing rig. Being able to adjust the DPI on the fly was very handy as I could drop it down to 400 for slow-mo movements, or increase it to 26,000 for ultra-fast movements. When I was in close-quarters combat in either game, I opted for a high DPI as I could then move and attack quickly, especially when surrounded by multiple enemies at once. This enabled me to change directions speedily and attack NPCs with, say, my knife — or whatever weapon I had equipped.</p><p>But when it came to using a big weapon like a sniper rifle to take out targets at a distance, I dropped the DPI by pressing the designated button. This helped me slow down my movements and I could aim better. When there multiple NPCs standing together and I needed to take out just one target, I was able to fine-tune my movements and execute a precise headshot, move to the next enemy, rinse and repeat. They never saw me coming.</p><p>In addition to variable DPI, the M64 Wireless packs a 1,000Hz polling rate too, which negates latency issues and input lag. 1,000Hz polling is enough for casual players, but if you’re a competitive gamer who partakes in high-refresh-rate gaming, you may want to consider a 4,000Hz mouse like the SteelSeries Aerox 3 Wireless Gen 2, or an 8,000Hz mouse like the Keychron M6 8K.</p><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-the-downs"><span>Cherry XTRFY M64 Wireless review: The downs</span></h2><p>Unfortunately, the Cherry XTRFY M64 Wireless doesn’t feature Bluetooth or companion software, and the awkwardly-placed USB-C port makes it difficult to use the mouse wired.</p><h2 id="no-bluetooth">No Bluetooth</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="N8CEYFiHH5tX5Yi22NizcJ" name="Cherry_M64_mouse 7.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/N8CEYFiHH5tX5Yi22NizcJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Surprisingly, the Cherry XTRFY M64 Wireless doesn’t feature Bluetooth. Now, I’ve used and tested my fair share of mice, and this is probably the first time I’ve seen a wireless mouse not pack in Bluetooth connectivity. Cherry advertises this as a competitive gaming mouse so I can kinda see why there’s no Bluetooth. Polling over Bluetooth is, more often than not, limited to 125Hz, so realistically, no gamer would be using it.</p><p>But does that justify its absence? Not entirely. We’ve seen similarly priced mice, like the SteelSeries Aerox 3 Wireless Gen 2, feature Bluetooth connectivity. It’s nice for those who work or play across systems. The only way to connect the M64 Wireless wirelessly is by using the included 2.4GHz dongle… but there’s no slot on the mouse’s body to slot the dongle into, so be careful you don’t lose it!</p><h2 id="awkwardly-placed-port">Awkwardly-placed port</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QrFyxctgUbKsAL5S4QsegJ" name="Cherry_M64_mouse 4.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/QrFyxctgUbKsAL5S4QsegJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 wireless connectivity, the Cherry XTRFY M64 Wireless can be used wired too, thanks to the included USB-C cable. But what’s frustrating is the USB-C port’s location on the mouse’s body. For some reason, Cherry decided to put it alongside the right-hand edge of the mouse.</p><p>To say it’s awkward and makes the mouse practically unusable as your hand keeps pressing against it would be an understatement. What gives, Cherry? I suppose the silver lining here is that Cherry didn’t decide to place it on the mouse’s underside (looking at you, Apple Magic Mouse).</p><p>If you want a great mouse with port placement that makes sense, I’d recommend the SteelSeries Aerox 3 Wireless Gen 2, the Keychron M6 8K or the <a href="https://www.tomsguide.com/gaming/peripherals/keychron-m7-review">Keychron M7</a> ($49). All are excellent alternatives and they feature tri-mode connectivity.</p><h2 id="no-companion-software">No companion software</h2><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="ZJNoZUjJ5PAGCCRs6XnyaJ" name="Cherry_M64_mouse 3.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:241,l:148,cw:960,ch:540,q:80/ZJNoZUjJ5PAGCCRs6XnyaJ.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 thing that surprises me about the Cherry XTRFY M64 Wireless is its lack of companion software. Sure, you can change the DPI, debounce time and polling rate via the buttons on the top and the mouse’s underside, but to not have <em>any </em>form of companion software for further customization is disappointing. </p><p>I’d have liked to have been able to assign custom values to the DPI and polling rate buttons, instead of the set of four (each) preloaded ones.</p><h2 id="expensive-for-what-s-on-offer">Expensive for what’s on offer</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="N6bqKGqizXDNeAg5VN6KHJ" name="Cherry_M64_mouse 5.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/N6bqKGqizXDNeAg5VN6KHJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the Cherry XTRFY M64 Wireless is an expensive mouse. Its pros simply fail to outweigh the cons. The lack of companion software and Bluetooth, as well as an awkwardly placed port make it difficult to justify the M64 Wireless’ premium price. It retails for <a href="https://www.amazon.com/CHERRY-XTRFY-Wireless-Ergonomic-Gaming/dp/B0DB2X74LD/" target="_blank" rel="nofollow">$99</a> / <a href="https://www.amazon.co.uk/CHERRY-XTRFY-Ultralight-Optimised-Ergonomic/dp/B0D4548YVZ/" target="_blank" rel="nofollow">£84 at Amazon</a> — that’s quite a bit!</p><p>Thing is, mice have come leaps and bounds in recent years, and you’ve got brands like Keychron making fantastic mice for less than $70 — case in point, the Keychron M6 8K and the Keychron M7. The M6 8K even offers 8,000Hz polling while the M64 Wireless tops out at 1,000Hz. If the M64 Wireless was a little cheaper, it would have been a different story. But as things stand, I find it difficult to recommend it outright when it faces such fierce competition.</p><h2 class="article-body__section" id="section-cherry-xtrfy-m64-wireless-review-verdict"><span>Cherry XTRFY M64 Wireless 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="2777B56QsMBacKYjqqzSLJ" name="Cherry_M64_mouse 1.JPG" alt="A black and teal Cherry XTRFY M64 Wireless gaming mouse" src="https://cdn.mos.cms.futurecdn.net/2777B56QsMBacKYjqqzSLJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Cherry XTRFY M64 Wireless isn’t without its compromises. There’s no Bluetooth so wireless connectivity is limited to 2.4GHz. The lack of companion software means you can’t customize the buttons and their respective functions. And don’t get me started on the placement of the USB-C port.</p><p>But this doesn’t mean that the M64 Wireless is <em>all </em>bad. For the price, you get a comfortable and minimalistically designed mouse that’s a joy to use. 26,000 DPI and 1,000Hz polling also make it great for casual gaming.</p><p>Unfortunately, those strengths just don’t make up for the M64 Wireless’ weaknesses, and when the mouse faces such fierce competition from the likes of Keychron, it’s difficult to recommend it at its retail price.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I love the Cherry KW 300 MX’s responsive switches and thocky sound profile — but at this price, I’d expect everything to be perfect ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> can elevate your typing experience and can even make you a better typist. The Cherry KW 300 MX tries to do just that — and succeeds, for the most part. </p><p>This is a compact 75% keyboard that you can travel with, or plop on your crowded desk without sacrificing too much space. It offers a lovely typing experience and a delightful thocky sound profile. It’s got nearly everything you need.</p><p>But the KW 300 MX isn’t perfect. Though the keyboard comes with macOS-specific keycaps, the companion software is confusingly Windows-only. The white-only backlighting is also quite dim, and frankly, you can get more bang for your buck elsewhere.</p><p>For the complete breakdown, read my full Cherry KW 300 MX review.</p><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-specs"><span>Cherry KW 300 MX 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/Cherry-Mechanical-hot-swappable-Sound-Proofed-Connection/dp/B0FKFFV19K/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/CHERRY-Mechanical-Hot-Swap-Keyboard-Illumination/dp/B0DTKJCDYL/" target="_blank" rel="nofollow">£139</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Cherry MX2A Brown (tactile), Red (linear)</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 82 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>White</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.2, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable Lithium-ion</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.59 x 5.31 x 1.53 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.64lbs</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-cherry-kw-300-mx-review-cheat-sheet"><span>Cherry KW 300 MX review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A mechanical keyboard with a 75% layout and hot-swappable switches</li><li><strong>Who is it for? </strong>Hardcore typists and casual gamers</li><li><strong>How much does it cost? </strong>The Cherry KW 300 MX is available for <a href="https://www.amazon.com/Cherry-Mechanical-hot-swappable-Sound-Proofed-Connection/dp/B0FKFFV19K/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/CHERRY-Mechanical-Hot-Swap-Keyboard-Illumination/dp/B0DTKJCDYL/" target="_blank" rel="nofollow">£139</a></li><li><strong>What do we like?</strong> The lovely typing experience and sound profile, great build, compact design, and inclusion of macOS keycaps</li><li><strong>What don’t we like? </strong>The Windows-only software, dim white backlighting, and price tag</li></ul><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-the-ups"><span>Cherry KW 300 MX review: The ups</span></h2><p>From its compact design and sturdy build, to its lovely typing experience and sound profile, the Cherry KW 300 MX is a fantastic mechanical keyboard.</p><h2 id="compact-design">Compact 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="f2pUnzYas5SYPXgQA5Hnbc" name="Cherry_KW_300W_MX_.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/f2pUnzYas5SYPXgQA5Hnbc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 looking for a mechanical keyboard that you can travel with and one that doesn’t take up too much space on your desk, the Cherry KW 300 MX is perfect. Sporting a 75% layout which foregoes the numberpad to save on space, the KW 300 MX measures 12.59 x 5.31 x 1.53 inches. It isn’t very heavy either, weighing just 1.64lbs. I’ve had no problem carrying it in my backpack, to and from work, even with plenty of other gadgets already in there.</p><p>Similar to some other mechanical keyboards I’ve tested, the KW 300 MX features a knob between its arrow and page navigation keys. You can use this knob to adjust the volume, backlighting’s brightness, and turn the keyboard on and off. I’d say the KW 300 MX’s knob is far more useful than the pricier <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a>’ ($259) which requires you to use confusing key combos to change settings on the fly.</p><h2 id="great-build-quality">Great build 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="xFRQJjxyxfG8nckeAbz7Lc" name="Cherry_KW_300W_MX_ 2.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/xFRQJjxyxfG8nckeAbz7Lc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 because the Cherry KW 300 MX is light doesn’t mean it’s flimsy — quite the opposite, in fact. The keyboard is built very well, and you can tell that the ABS plastic it uses in its construction isn’t cheap. Tapping the keyboard’s body with my fingernails didn’t reveal any hollow sounding parts, and much like the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a> ($74), there isn’t a lot of flex in the body either. </p><p>Adding to the KW 300 MX’s great build quality is its use of double-shot PBT keycaps which are more resistant to oil as well as wear and tear than their ABS counterparts. The keyboard also utilizes a gasket-mount design which helps with structural integrity. And that knob I mentioned earlier? It’s well-built too, and has just the right amount of resistance to keep it from feeling fragile.</p><h2 id="lovely-to-type-on-2">Lovely to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3EwhF74fW3d4niPqXFApVc" name="Cherry_KW_300W_MX_ 6.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/3EwhF74fW3d4niPqXFApVc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 bread and butter relies on me typing nearly 10,000 words a week, so I need a capable and competent keyboard that makes it effortless to do so. The Cherry KW 300 MX does just that. </p><p>This is a stunning keyboard to type on, and I even wrote this entire review on it. The KW 300 MX can be configured with either Cherry MX2A Brown (tactile) or Red (linear) switches. If you don’t like those, the keyboard is hot-swappable so you can change the switches without de- and re-soldering them to the PCB.</p><p>I tested the Red linears which require 45g of force to activate, and they have a pre-travel distance of 2mm (total of 4mm) so you don’t need to press them all the way down for input to register. This means that it’s not fatiguing to type on the keyboard, and after a full day’s worth of typing, I can confirm that my fingers were absolutely fine at the end of the day. I much prefer these light switches over, say, the 57g Bananas in the Keychron V1 8K — though they’re not as light as the 35g Sea Salts in the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115).</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Cherry KW 300 MX</p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.58</p></td><td  ><p>91.2%</p></td></tr></tbody></table></div><p>I think 45g of force is the sweet spot, and the KW 300 MX hasn’t given me much to complain about in terms of the typing experience. I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> to see how the keyboard fared, especially compared to other mechanical keyboards I’d tested just before the KW 300 MX. As you can see from the table above, my type test speed is above average, and my accuracy is good too. I’m pleased with these results.</p><h2 id="delightful-sound-profile">Delightful sound profile</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RXTkfQZHcSwg5pgahaAFGc" name="Cherry_KW_300W_MX_ 5.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/RXTkfQZHcSwg5pgahaAFGc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest pre-purchase considerations for keyboard enthusiasts is the sound profile. Does the keyboard sound creamy? Thocky? Clicky? Poppy? Quiet? The possibilities are endless. If you’re after a thocky deck, the Cherry KW 300 MX will be right up your street. The keyboard utilizes a gasket-mount design which reduces vibrations from key presses, and it packs several layers of sound dampening foam. All of this works in conjunction with the keyboard’s double-shot PBT keycaps to deliver a lovely thocky sound.</p><p>Cherry claims that the KW 300 MX’s sound-proofed housing offers “quiet operation” — I wouldn’t necessarily say it’s “quiet.” The KW 300 MX’s thockiness isn’t <em>too</em> loud, so it won’t disturb your coworkers, but it’s just loud enough for you to appreciate it.</p><h2 id="macos-keycaps-included">macOS keycaps included</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="o84SVjVbmXk7MriFTcg9Kc" name="Cherry_KW_300W_MX_ 4.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/o84SVjVbmXk7MriFTcg9Kc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, like me, you work across different operating systems, you may want a keyboard that boasts cross-platform compatibility. The Cherry KW 300 MX is one such keyboard. It comes fitted with Windows-specific keycaps but Cherry includes macOS ones in the box, so you can swap out the Windows key for the CMD key.</p><p>You can swap between operating system layouts easily too, by simply holding Fn + M for macOS and Fn + W for Windows. It takes three seconds for the KW 300 MX to switch to either layout, so it’s extremely easy and quick to do.</p><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-the-downs"><span>Cherry KW 300 MX review: The downs</span></h2><p>The Cherry KW 300 MX has some flaws, such as the Windows-only software, dim white backlighting, and premium price.</p><h2 id="windows-only-software">Windows-only software</h2><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="QQDoqTDUriJKHGbNDDp5Km" name="Cherry-KW-app" alt="Cherry Keys software screenshot" src="https://cdn.mos.cms.futurecdn.net/QQDoqTDUriJKHGbNDDp5Km-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/QQDoqTDUriJKHGbNDDp5Km-1920-80.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: Cherry / Tom's Guide)</span></figcaption></figure><p>The Cherry KW 300 MX is a multi-platform keyboard, so it’s compatible with Windows and macOS machines as well as Android smartphones. Like I mentioned earlier, Cherry happily included macOS keycaps in the box. But what’s disappointing is that there’s no macOS-compatible companion software.<a href="https://www.cherry.de/en-gb/products/software-services/cherry-keys" target="_blank" rel="nofollow"> Cherry Keys</a> is available on Windows only, and it feels like a massive oversight from the brand.</p><p>Cherry Keys itself is a user-friendly app. It sports a clean interface and everything is signposted well. You can use it to remap keys, record macros, and update/flash the KW 300 MX’s firmware. </p><p>It’s just a shame that macOS users can’t do this — and Cherry could have easily solved this issue with either cross-platform software or a web-based app. If you want a macOS-friendly keyboard that can be customized without the need for a Windows machine, check out the Keychron V1 8K or the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th-80-pro-review">Epomaker TH80 Pro</a> ($99).</p><h2 id="white-backlighting-only">White backlighting only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5guoZTfZw2TZWfBDSkwEec" name="Cherry_KW_300W_MX_ 1.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/5guoZTfZw2TZWfBDSkwEec-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Fans of RGB lighting, you may want to consider other options. The Cherry KW 300 MX features south-facing backlighting, yes, and it’s visible through the see-through keycaps, but it’s white only. </p><p>There’s no per-key RGB which might be a dealbreaker for some. Also, it isn’t very bright, even at its brightest setting, which is a little disappointing. I didn’t even realize it was turned up to full brightness in a well-lit room — an issue I’ve never had with RGB keyboards before.</p><p>If you love RGB lighting, boy have I got many, many recommendations for you. The Keychron V1 8K, the Epomaker TH80 Pro, and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3 </a>($139) are all excellent alternatives, and they cost less than the KW 300 MX — which brings me to my next point.</p><h2 id="a-little-expensive">A little expensive</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="oLUxjBdzV6mczLeKumdJ9c" name="Cherry_KW_300W_MX_ 7.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/oLUxjBdzV6mczLeKumdJ9c-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Mechanical keyboards have come leaps and bounds in recent years, and today, you can get a powerful mech deck for not a lot of money. The Cherry KW 300 MX isn’t one of them, sadly. Look, this is a good keyboard, and I’ve thoroughly enjoyed testing it, but the fact that it costs nearly $150 is a little off-putting. This keyboard retails for <a href="https://www.amazon.com/Cherry-Mechanical-hot-swappable-Sound-Proofed-Connection/dp/B0FKFFV19K/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/CHERRY-Mechanical-Hot-Swap-Keyboard-Illumination/dp/B0DTKJCDYL/" target="_blank" rel="nofollow">£139 at Amazon</a>, and as I wrote about recently, <a href="https://www.tomsguide.com/computing/keyboards/you-dont-need-to-break-the-bank-to-get-a-powerful-keyboard-here-are-my-top-7-picks-under-usd120-including-mechanical-and-hall-effect-decks">you don’t need to spend that much to get a decent keyboard</a>.</p><p>Not convinced by the KW 300 MX’s price? Don’t worry: there are plenty of cheaper alternatives out there. If you want a 75% layout keyboard with 1,000Hz polling and hot-swappable switches, the Keychron V1 8K ($74) is a fantastic option, and even features 8,000Hz polling (though it’s wired-only). Prefer a wireless deck? The NuPhy Air75 V3 ($139), the <a href="https://www.tomsguide.com/computing/keyboards/gravastar-mercury-k1-review">GravaStar Mercury K1</a> ($129), and the <a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-pro-review">Lemokey P1 Pro</a> ($129) offer outstanding value for money. Sub-$100 options are available too, like the 5-star <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99).</p><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-verdict"><span>Cherry KW 300 MX 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="rXvnWbdCxQkFEb2z6YmXzb" name="Cherry_KW_300W_MX_ 9.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/rXvnWbdCxQkFEb2z6YmXzb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 looking for a keyboard that’s simply wonderful to type on, you’ll love the Cherry KW 300 MX. Its MX2A Red linear switches that I tested are extremely responsive, and they’re a joy to type on. The keyboard features a delightful, thocky sound profile too, and its compact form factor makes it ideal for those short on space.</p><p>While that’s great, the KW 300 MX doesn’t offer the best value for money. It comes with macOS keycaps but no macOS-compatible software. The white backlighting may be disappointing to RGB fans, and it’s quite dim too. Given that many excellent keyboards cost less these days, you’d be better off getting a Keychron, NuPhy or Epomaker. The KW 300 MX is good, yes, but it could have been great.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/cherry-kw-300-mx-review</link>
                                                                            <description>
                            <![CDATA[ The Cherry KW 300 MX offers a lovely typing experience, thocky sound profile, and comes with macOS keycaps. But its drawbacks leave a lot to be desired. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">EWVCQPqpDViQFWdEcGiEBW</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/U6SN46iGYnCGqzVUMxBSSc-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Thu, 14 May 2026 12:37:43 +0000</pubDate>                                                                                                                                <updated>Fri, 15 May 2026 13:40:17 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/U6SN46iGYnCGqzVUMxBSSc-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Cherry KW 300 MX mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A Cherry KW 300 MX mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A Cherry KW 300 MX mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/U6SN46iGYnCGqzVUMxBSSc-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> can elevate your typing experience and can even make you a better typist. The Cherry KW 300 MX tries to do just that — and succeeds, for the most part. </p><p>This is a compact 75% keyboard that you can travel with, or plop on your crowded desk without sacrificing too much space. It offers a lovely typing experience and a delightful thocky sound profile. It’s got nearly everything you need.</p><p>But the KW 300 MX isn’t perfect. Though the keyboard comes with macOS-specific keycaps, the companion software is confusingly Windows-only. The white-only backlighting is also quite dim, and frankly, you can get more bang for your buck elsewhere.</p><p>For the complete breakdown, read my full Cherry KW 300 MX review.</p><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-specs"><span>Cherry KW 300 MX 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/Cherry-Mechanical-hot-swappable-Sound-Proofed-Connection/dp/B0FKFFV19K/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/CHERRY-Mechanical-Hot-Swap-Keyboard-Illumination/dp/B0DTKJCDYL/" target="_blank" rel="nofollow">£139</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Cherry MX2A Brown (tactile), Red (linear)</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 82 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>White</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth 5.2, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable Lithium-ion</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.59 x 5.31 x 1.53 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.64lbs</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-cherry-kw-300-mx-review-cheat-sheet"><span>Cherry KW 300 MX review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A mechanical keyboard with a 75% layout and hot-swappable switches</li><li><strong>Who is it for? </strong>Hardcore typists and casual gamers</li><li><strong>How much does it cost? </strong>The Cherry KW 300 MX is available for <a href="https://www.amazon.com/Cherry-Mechanical-hot-swappable-Sound-Proofed-Connection/dp/B0FKFFV19K/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/CHERRY-Mechanical-Hot-Swap-Keyboard-Illumination/dp/B0DTKJCDYL/" target="_blank" rel="nofollow">£139</a></li><li><strong>What do we like?</strong> The lovely typing experience and sound profile, great build, compact design, and inclusion of macOS keycaps</li><li><strong>What don’t we like? </strong>The Windows-only software, dim white backlighting, and price tag</li></ul><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-the-ups"><span>Cherry KW 300 MX review: The ups</span></h2><p>From its compact design and sturdy build, to its lovely typing experience and sound profile, the Cherry KW 300 MX is a fantastic mechanical keyboard.</p><h2 id="compact-design">Compact 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="f2pUnzYas5SYPXgQA5Hnbc" name="Cherry_KW_300W_MX_.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/f2pUnzYas5SYPXgQA5Hnbc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 looking for a mechanical keyboard that you can travel with and one that doesn’t take up too much space on your desk, the Cherry KW 300 MX is perfect. Sporting a 75% layout which foregoes the numberpad to save on space, the KW 300 MX measures 12.59 x 5.31 x 1.53 inches. It isn’t very heavy either, weighing just 1.64lbs. I’ve had no problem carrying it in my backpack, to and from work, even with plenty of other gadgets already in there.</p><p>Similar to some other mechanical keyboards I’ve tested, the KW 300 MX features a knob between its arrow and page navigation keys. You can use this knob to adjust the volume, backlighting’s brightness, and turn the keyboard on and off. I’d say the KW 300 MX’s knob is far more useful than the pricier <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a>’ ($259) which requires you to use confusing key combos to change settings on the fly.</p><h2 id="great-build-quality">Great build 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="xFRQJjxyxfG8nckeAbz7Lc" name="Cherry_KW_300W_MX_ 2.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/xFRQJjxyxfG8nckeAbz7Lc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 because the Cherry KW 300 MX is light doesn’t mean it’s flimsy — quite the opposite, in fact. The keyboard is built very well, and you can tell that the ABS plastic it uses in its construction isn’t cheap. Tapping the keyboard’s body with my fingernails didn’t reveal any hollow sounding parts, and much like the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a> ($74), there isn’t a lot of flex in the body either. </p><p>Adding to the KW 300 MX’s great build quality is its use of double-shot PBT keycaps which are more resistant to oil as well as wear and tear than their ABS counterparts. The keyboard also utilizes a gasket-mount design which helps with structural integrity. And that knob I mentioned earlier? It’s well-built too, and has just the right amount of resistance to keep it from feeling fragile.</p><h2 id="lovely-to-type-on-2">Lovely to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="3EwhF74fW3d4niPqXFApVc" name="Cherry_KW_300W_MX_ 6.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/3EwhF74fW3d4niPqXFApVc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 bread and butter relies on me typing nearly 10,000 words a week, so I need a capable and competent keyboard that makes it effortless to do so. The Cherry KW 300 MX does just that. </p><p>This is a stunning keyboard to type on, and I even wrote this entire review on it. The KW 300 MX can be configured with either Cherry MX2A Brown (tactile) or Red (linear) switches. If you don’t like those, the keyboard is hot-swappable so you can change the switches without de- and re-soldering them to the PCB.</p><p>I tested the Red linears which require 45g of force to activate, and they have a pre-travel distance of 2mm (total of 4mm) so you don’t need to press them all the way down for input to register. This means that it’s not fatiguing to type on the keyboard, and after a full day’s worth of typing, I can confirm that my fingers were absolutely fine at the end of the day. I much prefer these light switches over, say, the 57g Bananas in the Keychron V1 8K — though they’re not as light as the 35g Sea Salts in the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115).</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Cherry KW 300 MX</p></td><td  ><p>94</p></td><td  ><p>90.4%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review">Keychron V1 8K</a></p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.58</p></td><td  ><p>91.2%</p></td></tr></tbody></table></div><p>I think 45g of force is the sweet spot, and the KW 300 MX hasn’t given me much to complain about in terms of the typing experience. I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> to see how the keyboard fared, especially compared to other mechanical keyboards I’d tested just before the KW 300 MX. As you can see from the table above, my type test speed is above average, and my accuracy is good too. I’m pleased with these results.</p><h2 id="delightful-sound-profile">Delightful sound profile</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="RXTkfQZHcSwg5pgahaAFGc" name="Cherry_KW_300W_MX_ 5.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/RXTkfQZHcSwg5pgahaAFGc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest pre-purchase considerations for keyboard enthusiasts is the sound profile. Does the keyboard sound creamy? Thocky? Clicky? Poppy? Quiet? The possibilities are endless. If you’re after a thocky deck, the Cherry KW 300 MX will be right up your street. The keyboard utilizes a gasket-mount design which reduces vibrations from key presses, and it packs several layers of sound dampening foam. All of this works in conjunction with the keyboard’s double-shot PBT keycaps to deliver a lovely thocky sound.</p><p>Cherry claims that the KW 300 MX’s sound-proofed housing offers “quiet operation” — I wouldn’t necessarily say it’s “quiet.” The KW 300 MX’s thockiness isn’t <em>too</em> loud, so it won’t disturb your coworkers, but it’s just loud enough for you to appreciate it.</p><h2 id="macos-keycaps-included">macOS keycaps included</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="o84SVjVbmXk7MriFTcg9Kc" name="Cherry_KW_300W_MX_ 4.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/o84SVjVbmXk7MriFTcg9Kc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, like me, you work across different operating systems, you may want a keyboard that boasts cross-platform compatibility. The Cherry KW 300 MX is one such keyboard. It comes fitted with Windows-specific keycaps but Cherry includes macOS ones in the box, so you can swap out the Windows key for the CMD key.</p><p>You can swap between operating system layouts easily too, by simply holding Fn + M for macOS and Fn + W for Windows. It takes three seconds for the KW 300 MX to switch to either layout, so it’s extremely easy and quick to do.</p><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-the-downs"><span>Cherry KW 300 MX review: The downs</span></h2><p>The Cherry KW 300 MX has some flaws, such as the Windows-only software, dim white backlighting, and premium price.</p><h2 id="windows-only-software">Windows-only software</h2><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="QQDoqTDUriJKHGbNDDp5Km" name="Cherry-KW-app" alt="Cherry Keys software screenshot" src="https://cdn.mos.cms.futurecdn.net/QQDoqTDUriJKHGbNDDp5Km-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/QQDoqTDUriJKHGbNDDp5Km-1920-80.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: Cherry / Tom's Guide)</span></figcaption></figure><p>The Cherry KW 300 MX is a multi-platform keyboard, so it’s compatible with Windows and macOS machines as well as Android smartphones. Like I mentioned earlier, Cherry happily included macOS keycaps in the box. But what’s disappointing is that there’s no macOS-compatible companion software.<a href="https://www.cherry.de/en-gb/products/software-services/cherry-keys" target="_blank" rel="nofollow"> Cherry Keys</a> is available on Windows only, and it feels like a massive oversight from the brand.</p><p>Cherry Keys itself is a user-friendly app. It sports a clean interface and everything is signposted well. You can use it to remap keys, record macros, and update/flash the KW 300 MX’s firmware. </p><p>It’s just a shame that macOS users can’t do this — and Cherry could have easily solved this issue with either cross-platform software or a web-based app. If you want a macOS-friendly keyboard that can be customized without the need for a Windows machine, check out the Keychron V1 8K or the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th-80-pro-review">Epomaker TH80 Pro</a> ($99).</p><h2 id="white-backlighting-only">White backlighting only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5guoZTfZw2TZWfBDSkwEec" name="Cherry_KW_300W_MX_ 1.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/5guoZTfZw2TZWfBDSkwEec-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Fans of RGB lighting, you may want to consider other options. The Cherry KW 300 MX features south-facing backlighting, yes, and it’s visible through the see-through keycaps, but it’s white only. </p><p>There’s no per-key RGB which might be a dealbreaker for some. Also, it isn’t very bright, even at its brightest setting, which is a little disappointing. I didn’t even realize it was turned up to full brightness in a well-lit room — an issue I’ve never had with RGB keyboards before.</p><p>If you love RGB lighting, boy have I got many, many recommendations for you. The Keychron V1 8K, the Epomaker TH80 Pro, and the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3 </a>($139) are all excellent alternatives, and they cost less than the KW 300 MX — which brings me to my next point.</p><h2 id="a-little-expensive">A little expensive</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="oLUxjBdzV6mczLeKumdJ9c" name="Cherry_KW_300W_MX_ 7.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/oLUxjBdzV6mczLeKumdJ9c-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Mechanical keyboards have come leaps and bounds in recent years, and today, you can get a powerful mech deck for not a lot of money. The Cherry KW 300 MX isn’t one of them, sadly. Look, this is a good keyboard, and I’ve thoroughly enjoyed testing it, but the fact that it costs nearly $150 is a little off-putting. This keyboard retails for <a href="https://www.amazon.com/Cherry-Mechanical-hot-swappable-Sound-Proofed-Connection/dp/B0FKFFV19K/" target="_blank" rel="nofollow">$149</a> / <a href="https://www.amazon.co.uk/CHERRY-Mechanical-Hot-Swap-Keyboard-Illumination/dp/B0DTKJCDYL/" target="_blank" rel="nofollow">£139 at Amazon</a>, and as I wrote about recently, <a href="https://www.tomsguide.com/computing/keyboards/you-dont-need-to-break-the-bank-to-get-a-powerful-keyboard-here-are-my-top-7-picks-under-usd120-including-mechanical-and-hall-effect-decks">you don’t need to spend that much to get a decent keyboard</a>.</p><p>Not convinced by the KW 300 MX’s price? Don’t worry: there are plenty of cheaper alternatives out there. If you want a 75% layout keyboard with 1,000Hz polling and hot-swappable switches, the Keychron V1 8K ($74) is a fantastic option, and even features 8,000Hz polling (though it’s wired-only). Prefer a wireless deck? The NuPhy Air75 V3 ($139), the <a href="https://www.tomsguide.com/computing/keyboards/gravastar-mercury-k1-review">GravaStar Mercury K1</a> ($129), and the <a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-pro-review">Lemokey P1 Pro</a> ($129) offer outstanding value for money. Sub-$100 options are available too, like the 5-star <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99).</p><h2 class="article-body__section" id="section-cherry-kw-300-mx-review-verdict"><span>Cherry KW 300 MX 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="rXvnWbdCxQkFEb2z6YmXzb" name="Cherry_KW_300W_MX_ 9.JPG" alt="A Cherry KW 300 MX mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/rXvnWbdCxQkFEb2z6YmXzb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 looking for a keyboard that’s simply wonderful to type on, you’ll love the Cherry KW 300 MX. Its MX2A Red linear switches that I tested are extremely responsive, and they’re a joy to type on. The keyboard features a delightful, thocky sound profile too, and its compact form factor makes it ideal for those short on space.</p><p>While that’s great, the KW 300 MX doesn’t offer the best value for money. It comes with macOS keycaps but no macOS-compatible software. The white backlighting may be disappointing to RGB fans, and it’s quite dim too. Given that many excellent keyboards cost less these days, you’d be better off getting a Keychron, NuPhy or Epomaker. The KW 300 MX is good, yes, but it could have been great.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ You don’t need to break the bank to get a powerful keyboard — here are my top 7 picks under $120, including mechanical and Hall Effect decks ]]></title>
                                                                                                <dc:content><![CDATA[ <p>If you aren't well-versed with the vast and expansive world of keyboards, you might think that a good keyboard costs hundreds and hundreds of dollars — but that couldn't be further from the truth. While some keyboards from the likes of Cherry and Corsair can cost nearly $200, you don't need to spend more than $120 to get a decent deck. Trust me, it's my job to test the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards</a>, and I personally believe most humans don't need to break the bank for them.</p><p>Before diving into my top picks, I want you to ask yourself, "What do I need this keyboard for?" It could be for competitive gaming, hardcore productivity and typing, or a mix of both. You'd be surprised at how many sub-$120 keyboards exist, and just how outstanding they are (and they even put pricier ones to shame!).</p><p>After having tested over 80 different keyboards — mechanical, Hall Effect, optical, you name it — I was spoilt for choice picking the following keebs. The following keyboards are rated 4-stars or higher by yours truly. So what are you waiting for? Add them to your cart now!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xm4P0O"></div>                            </div>                            <script src="https://kwizly.com/embed/Xm4P0O.js" async></script><div class="product"><a data-dimension112="151051c7-b4ee-4cf7-908e-b078f0651f05" data-action="Deal Block" data-label="V1 8K" data-dimension48="V1 8K" data-dimension25="$74" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" 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:98.20%;"><img id="MUsBzDHFVfHhDcMypVef63" name="Keychron-V1-8K-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/MUsBzDHFVfHhDcMypVef63-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1473" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Keychron has upgraded one of its best mechanical keyboards and added 8,000Hz polling and extensive RGB customization to the V1 8K. This deck is an absolute winner. Offering a lovely typing experience and sound profile, as well as great build quality, the V1 8K doesn’t give you much to complain about. Priced at just $74, it sets a new standard for budget mechanical decks.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow" data-dimension112="151051c7-b4ee-4cf7-908e-b078f0651f05" data-action="Deal Block" data-label="V1 8K" data-dimension48="V1 8K" data-dimension25="$74">View Deal</a></p></div><div class="product"><a data-dimension112="34161f4e-6ae2-4956-bb91-fb907dc9a61f" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$55" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1497px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="cJXmt94ox3BRqMu7Lk5oyk" name="Hexgears Immersion A3 TKL" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/cJXmt94ox3BRqMu7Lk5oyk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1497" height="1497" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review" data-dimension112="34161f4e-6ae2-4956-bb91-fb907dc9a61f" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$55">Hexgears Immersion A3 TKL</a> is a fantastic low-profile keyboard for productivity, thanks to the super silent Kailh linear switches. Its 1,000Hz polling rate is also well-suited to casual gaming, and its sturdy build and customizable RGB and TFT screen make it a joy to use.<a class="view-deal button" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow" data-dimension112="34161f4e-6ae2-4956-bb91-fb907dc9a61f" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$55">View Deal</a></p></div><div class="product"><a data-dimension112="47da834c-b012-40a9-8a1f-c666ab548c1d" data-action="Deal Block" data-label="Mercury K1 Lite" data-dimension48="Mercury K1 Lite" data-dimension25="$89" href="https://www.amazon.com/GravaStar-K1-Lite-Mechanical-Swappable/dp/B0DFXWQWB8/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:513px;"><p class="vanilla-image-block" style="padding-top:69.20%;"><img id="JBhYzpdBnsXiSK7337znya" name="Gravastar Mercury K1 Lite" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/JBhYzpdBnsXiSK7337znya-1920-80.jpg" mos="" align="middle" fullscreen="" width="513" height="355" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>GravaStar has just three boards on offer, and the <a href="https://www.tomsguide.com/computing/peripherals/gravastar-mercury-k1-lite-review" data-dimension112="47da834c-b012-40a9-8a1f-c666ab548c1d" data-action="Deal Block" data-label="Mercury K1 Lite" data-dimension48="Mercury K1 Lite" data-dimension25="$89">Mercury K1 Lite</a> is one of them — and the cheapest of the lot. It boasts sturdy construction, fantastic typing and casual gaming performance and comes with user-friendly companion software.<a class="view-deal button" href="https://www.amazon.com/GravaStar-K1-Lite-Mechanical-Swappable/dp/B0DFXWQWB8/" target="_blank" rel="nofollow" data-dimension112="47da834c-b012-40a9-8a1f-c666ab548c1d" data-action="Deal Block" data-label="Mercury K1 Lite" data-dimension48="Mercury K1 Lite" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="fcf2cca8-56da-471e-ab9c-1447fddd9924" data-action="Deal Block" data-label="ND75 LP" data-dimension48="ND75 LP" data-dimension25="$96" href="https://www.amazon.com/Aluminum-Mechanical-Keyboard-Wireless-Disassembly/dp/B0DLWL65S9/" 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:62.53%;"><img id="ga4jNCrnPkfUfsxEDvDcnZ" name="ND75-LP-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ga4jNCrnPkfUfsxEDvDcnZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="938" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>For modding enthusiasts who don’t want to spend hundreds on a keyboard, the <a href="https://www.tomsguide.com/computing/peripherals/chilkey-nd75-lp-review" data-dimension112="fcf2cca8-56da-471e-ab9c-1447fddd9924" data-action="Deal Block" data-label="ND75 LP" data-dimension48="ND75 LP" data-dimension25="$96">Chilkey ND75 LP</a> is a fantastic option. Alongside great customization capabilities, this low-profile keyboard is comfortable and it’s built like a tank. It offers a wonderful typing experience and comes with web-based software.<a class="view-deal button" href="https://www.amazon.com/Aluminum-Mechanical-Keyboard-Wireless-Disassembly/dp/B0DLWL65S9/" target="_blank" rel="nofollow" data-dimension112="fcf2cca8-56da-471e-ab9c-1447fddd9924" data-action="Deal Block" data-label="ND75 LP" data-dimension48="ND75 LP" data-dimension25="$96">View Deal</a></p></div><div class="product"><a data-dimension112="6e7b1e3e-b893-4b8f-92a2-452b1b1bced2" data-action="Deal Block" data-label="X75 V2" data-dimension48="X75 V2" data-dimension25="$104" href="https://www.amazon.com/MCHOSE-X75-V2-Mechanical-Swappable/dp/B0DQ1681BB/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:466px;"><p class="vanilla-image-block" style="padding-top:41.20%;"><img id="8pwpkEL8zdcEZKQAhZbSi4" name="MCHOSE X75 V2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/8pwpkEL8zdcEZKQAhZbSi4-1920-80.jpg" mos="" align="middle" fullscreen="" width="466" height="192" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review" data-dimension112="6e7b1e3e-b893-4b8f-92a2-452b1b1bced2" data-action="Deal Block" data-label="X75 V2" data-dimension48="X75 V2" data-dimension25="$104">McHose X75 V2</a> is a fantastic productivity keyboard that also holds its own in casual gaming thanks to the responsive stock linear switches and 1,000Hz polling rate. It sports a retro design, comes with onboard dongle storage, packs intuitive companion software, and detects operating systems over Bluetooth.<a class="view-deal button" href="https://www.amazon.com/MCHOSE-X75-V2-Mechanical-Swappable/dp/B0DQ1681BB/" target="_blank" rel="nofollow" data-dimension112="6e7b1e3e-b893-4b8f-92a2-452b1b1bced2" data-action="Deal Block" data-label="X75 V2" data-dimension48="X75 V2" data-dimension25="$104">View Deal</a></p></div><div class="product"><a data-dimension112="75ae91b3-870e-4b43-87fe-dce34aa3f807" data-action="Deal Block" data-label="HE68" data-dimension48="HE68" data-dimension25="$53" href="https://www.amazon.com/EPOMAKER-HE68-Keyboard-Adjustable-Magnetic/dp/B0DSZXFYK5/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1493px;"><p class="vanilla-image-block" style="padding-top:31.08%;"><img id="QTcxLiZ7BfxRPreDSxR4ya" name="Epomaker-HE68-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/QTcxLiZ7BfxRPreDSxR4ya-1920-80.jpg" mos="" align="middle" fullscreen="" width="1493" height="464" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review" data-dimension112="75ae91b3-870e-4b43-87fe-dce34aa3f807" data-action="Deal Block" data-label="HE68" data-dimension48="HE68" data-dimension25="$53">Epomaker HE68 </a>is a stellar budget gaming keyboard, offering premium gaming features like 8,000Hz polling, per-key and quad-actuation and more. It looks lovely with its minimalist design, and it’s great to type on too. For $59, the HE68 doesn’t give you much to complain about.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-HE68-Keyboard-Adjustable-Magnetic/dp/B0DSZXFYK5/" target="_blank" rel="nofollow" data-dimension112="75ae91b3-870e-4b43-87fe-dce34aa3f807" data-action="Deal Block" data-label="HE68" data-dimension48="HE68" data-dimension25="$53">View Deal</a></p></div><div class="product"><a data-dimension112="797dea9f-7273-486c-a79b-953a8604c593" data-action="Deal Block" data-label="HE65 Mag" data-dimension48="HE65 Mag" data-dimension25="$99" href="http://amazon.com/EPOMAKER-HE65-Wireless-Actuation-Adjustable-Gasket-Mount/dp/B0DTJX16FY/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1464px;"><p class="vanilla-image-block" style="padding-top:36.13%;"><img id="wLw7U6a6KLQBUHCMfGFoqK" name="Epomaker HE65 Mag" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wLw7U6a6KLQBUHCMfGFoqK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1464" height="529" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review" data-dimension112="797dea9f-7273-486c-a79b-953a8604c593" data-action="Deal Block" data-label="HE65 Mag" data-dimension48="HE65 Mag" data-dimension25="$99">Epomaker HE65 Mag</a> is an extremely powerful keyboard, especially for its size. Not only does it have per-key and quad-actuation, Rapid Trigger and Snap Tap like pricier keyboards, it also features 8,000Hz polling over a 2.4GHz wireless connection. The keyboard is easy to customize via its companion software, and as long as you don’t mind switch choice being limited, it’s well worth the money.<a class="view-deal button" href="http://amazon.com/EPOMAKER-HE65-Wireless-Actuation-Adjustable-Gasket-Mount/dp/B0DTJX16FY/" target="_blank" rel="nofollow" data-dimension112="797dea9f-7273-486c-a79b-953a8604c593" data-action="Deal Block" data-label="HE65 Mag" data-dimension48="HE65 Mag" data-dimension25="$99">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/computing/peripherals/7-things-to-know-when-choosing-your-first-mechanical-keyboard">7 things to know when choosing your first mechanical keyboard</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-reviewed-a-usd230-usd120-and-usd80-mechanical-keyboard-this-week-this-is-the-only-one-id-buy">I reviewed a $230, $120 and $80 mechanical keyboard this week — this is the only one I’d buy</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/you-dont-need-to-break-the-bank-to-get-a-powerful-keyboard-here-are-my-top-7-picks-under-usd120-including-mechanical-and-hall-effect-decks</link>
                                                                            <description>
                            <![CDATA[ From mechanical productivity beasts to Hall Effect gaming powerhouses, there are plenty of keyboards out there, especially if you're on a tight sub-$120 budget. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">P52uB4xs5ohyBLiUiVFfh6</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/ziPjWBvBKWkpaWx7LeLGnm-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 11 May 2026 07:30:00 +0000</pubDate>                                                                                                                                <updated>Fri, 15 May 2026 13:43:13 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/ziPjWBvBKWkpaWx7LeLGnm-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Two keyboards side by side]]></media:description>                                                            <media:text><![CDATA[Two keyboards side by side]]></media:text>
                                <media:title type="plain"><![CDATA[Two keyboards side by side]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/ziPjWBvBKWkpaWx7LeLGnm-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>If you aren't well-versed with the vast and expansive world of keyboards, you might think that a good keyboard costs hundreds and hundreds of dollars — but that couldn't be further from the truth. While some keyboards from the likes of Cherry and Corsair can cost nearly $200, you don't need to spend more than $120 to get a decent deck. Trust me, it's my job to test the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards</a>, and I personally believe most humans don't need to break the bank for them.</p><p>Before diving into my top picks, I want you to ask yourself, "What do I need this keyboard for?" It could be for competitive gaming, hardcore productivity and typing, or a mix of both. You'd be surprised at how many sub-$120 keyboards exist, and just how outstanding they are (and they even put pricier ones to shame!).</p><p>After having tested over 80 different keyboards — mechanical, Hall Effect, optical, you name it — I was spoilt for choice picking the following keebs. The following keyboards are rated 4-stars or higher by yours truly. So what are you waiting for? Add them to your cart now!</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xm4P0O"></div>                            </div>                            <script src="https://kwizly.com/embed/Xm4P0O.js" async></script><div class="product"><a data-dimension112="151051c7-b4ee-4cf7-908e-b078f0651f05" data-action="Deal Block" data-label="V1 8K" data-dimension48="V1 8K" data-dimension25="$74" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" 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:98.20%;"><img id="MUsBzDHFVfHhDcMypVef63" name="Keychron-V1-8K-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/MUsBzDHFVfHhDcMypVef63-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="1473" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>Keychron has upgraded one of its best mechanical keyboards and added 8,000Hz polling and extensive RGB customization to the V1 8K. This deck is an absolute winner. Offering a lovely typing experience and sound profile, as well as great build quality, the V1 8K doesn’t give you much to complain about. Priced at just $74, it sets a new standard for budget mechanical decks.<a class="view-deal button" href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow" data-dimension112="151051c7-b4ee-4cf7-908e-b078f0651f05" data-action="Deal Block" data-label="V1 8K" data-dimension48="V1 8K" data-dimension25="$74">View Deal</a></p></div><div class="product"><a data-dimension112="34161f4e-6ae2-4956-bb91-fb907dc9a61f" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$55" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1497px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="cJXmt94ox3BRqMu7Lk5oyk" name="Hexgears Immersion A3 TKL" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/cJXmt94ox3BRqMu7Lk5oyk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1497" height="1497" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review" data-dimension112="34161f4e-6ae2-4956-bb91-fb907dc9a61f" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$55">Hexgears Immersion A3 TKL</a> is a fantastic low-profile keyboard for productivity, thanks to the super silent Kailh linear switches. Its 1,000Hz polling rate is also well-suited to casual gaming, and its sturdy build and customizable RGB and TFT screen make it a joy to use.<a class="view-deal button" href="https://www.amazon.com/Hexgears-Immersion-A3-Low-Profile-Hot-Swappable/dp/B0D986JN9F/" target="_blank" rel="nofollow" data-dimension112="34161f4e-6ae2-4956-bb91-fb907dc9a61f" data-action="Deal Block" data-label="Immersion A3 TKL" data-dimension48="Immersion A3 TKL" data-dimension25="$55">View Deal</a></p></div><div class="product"><a data-dimension112="47da834c-b012-40a9-8a1f-c666ab548c1d" data-action="Deal Block" data-label="Mercury K1 Lite" data-dimension48="Mercury K1 Lite" data-dimension25="$89" href="https://www.amazon.com/GravaStar-K1-Lite-Mechanical-Swappable/dp/B0DFXWQWB8/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:513px;"><p class="vanilla-image-block" style="padding-top:69.20%;"><img id="JBhYzpdBnsXiSK7337znya" name="Gravastar Mercury K1 Lite" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/JBhYzpdBnsXiSK7337znya-1920-80.jpg" mos="" align="middle" fullscreen="" width="513" height="355" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>GravaStar has just three boards on offer, and the <a href="https://www.tomsguide.com/computing/peripherals/gravastar-mercury-k1-lite-review" data-dimension112="47da834c-b012-40a9-8a1f-c666ab548c1d" data-action="Deal Block" data-label="Mercury K1 Lite" data-dimension48="Mercury K1 Lite" data-dimension25="$89">Mercury K1 Lite</a> is one of them — and the cheapest of the lot. It boasts sturdy construction, fantastic typing and casual gaming performance and comes with user-friendly companion software.<a class="view-deal button" href="https://www.amazon.com/GravaStar-K1-Lite-Mechanical-Swappable/dp/B0DFXWQWB8/" target="_blank" rel="nofollow" data-dimension112="47da834c-b012-40a9-8a1f-c666ab548c1d" data-action="Deal Block" data-label="Mercury K1 Lite" data-dimension48="Mercury K1 Lite" data-dimension25="$89">View Deal</a></p></div><div class="product"><a data-dimension112="fcf2cca8-56da-471e-ab9c-1447fddd9924" data-action="Deal Block" data-label="ND75 LP" data-dimension48="ND75 LP" data-dimension25="$96" href="https://www.amazon.com/Aluminum-Mechanical-Keyboard-Wireless-Disassembly/dp/B0DLWL65S9/" 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:62.53%;"><img id="ga4jNCrnPkfUfsxEDvDcnZ" name="ND75-LP-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ga4jNCrnPkfUfsxEDvDcnZ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="938" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>For modding enthusiasts who don’t want to spend hundreds on a keyboard, the <a href="https://www.tomsguide.com/computing/peripherals/chilkey-nd75-lp-review" data-dimension112="fcf2cca8-56da-471e-ab9c-1447fddd9924" data-action="Deal Block" data-label="ND75 LP" data-dimension48="ND75 LP" data-dimension25="$96">Chilkey ND75 LP</a> is a fantastic option. Alongside great customization capabilities, this low-profile keyboard is comfortable and it’s built like a tank. It offers a wonderful typing experience and comes with web-based software.<a class="view-deal button" href="https://www.amazon.com/Aluminum-Mechanical-Keyboard-Wireless-Disassembly/dp/B0DLWL65S9/" target="_blank" rel="nofollow" data-dimension112="fcf2cca8-56da-471e-ab9c-1447fddd9924" data-action="Deal Block" data-label="ND75 LP" data-dimension48="ND75 LP" data-dimension25="$96">View Deal</a></p></div><div class="product"><a data-dimension112="6e7b1e3e-b893-4b8f-92a2-452b1b1bced2" data-action="Deal Block" data-label="X75 V2" data-dimension48="X75 V2" data-dimension25="$104" href="https://www.amazon.com/MCHOSE-X75-V2-Mechanical-Swappable/dp/B0DQ1681BB/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:466px;"><p class="vanilla-image-block" style="padding-top:41.20%;"><img id="8pwpkEL8zdcEZKQAhZbSi4" name="MCHOSE X75 V2" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/8pwpkEL8zdcEZKQAhZbSi4-1920-80.jpg" mos="" align="middle" fullscreen="" width="466" height="192" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review" data-dimension112="6e7b1e3e-b893-4b8f-92a2-452b1b1bced2" data-action="Deal Block" data-label="X75 V2" data-dimension48="X75 V2" data-dimension25="$104">McHose X75 V2</a> is a fantastic productivity keyboard that also holds its own in casual gaming thanks to the responsive stock linear switches and 1,000Hz polling rate. It sports a retro design, comes with onboard dongle storage, packs intuitive companion software, and detects operating systems over Bluetooth.<a class="view-deal button" href="https://www.amazon.com/MCHOSE-X75-V2-Mechanical-Swappable/dp/B0DQ1681BB/" target="_blank" rel="nofollow" data-dimension112="6e7b1e3e-b893-4b8f-92a2-452b1b1bced2" data-action="Deal Block" data-label="X75 V2" data-dimension48="X75 V2" data-dimension25="$104">View Deal</a></p></div><div class="product"><a data-dimension112="75ae91b3-870e-4b43-87fe-dce34aa3f807" data-action="Deal Block" data-label="HE68" data-dimension48="HE68" data-dimension25="$53" href="https://www.amazon.com/EPOMAKER-HE68-Keyboard-Adjustable-Magnetic/dp/B0DSZXFYK5/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1493px;"><p class="vanilla-image-block" style="padding-top:31.08%;"><img id="QTcxLiZ7BfxRPreDSxR4ya" name="Epomaker-HE68-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/QTcxLiZ7BfxRPreDSxR4ya-1920-80.jpg" mos="" align="middle" fullscreen="" width="1493" height="464" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review" data-dimension112="75ae91b3-870e-4b43-87fe-dce34aa3f807" data-action="Deal Block" data-label="HE68" data-dimension48="HE68" data-dimension25="$53">Epomaker HE68 </a>is a stellar budget gaming keyboard, offering premium gaming features like 8,000Hz polling, per-key and quad-actuation and more. It looks lovely with its minimalist design, and it’s great to type on too. For $59, the HE68 doesn’t give you much to complain about.<a class="view-deal button" href="https://www.amazon.com/EPOMAKER-HE68-Keyboard-Adjustable-Magnetic/dp/B0DSZXFYK5/" target="_blank" rel="nofollow" data-dimension112="75ae91b3-870e-4b43-87fe-dce34aa3f807" data-action="Deal Block" data-label="HE68" data-dimension48="HE68" data-dimension25="$53">View Deal</a></p></div><div class="product"><a data-dimension112="797dea9f-7273-486c-a79b-953a8604c593" data-action="Deal Block" data-label="HE65 Mag" data-dimension48="HE65 Mag" data-dimension25="$99" href="http://amazon.com/EPOMAKER-HE65-Wireless-Actuation-Adjustable-Gasket-Mount/dp/B0DTJX16FY/" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1464px;"><p class="vanilla-image-block" style="padding-top:36.13%;"><img id="wLw7U6a6KLQBUHCMfGFoqK" name="Epomaker HE65 Mag" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/wLw7U6a6KLQBUHCMfGFoqK-1920-80.jpg" mos="" align="middle" fullscreen="" width="1464" height="529" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review" data-dimension112="797dea9f-7273-486c-a79b-953a8604c593" data-action="Deal Block" data-label="HE65 Mag" data-dimension48="HE65 Mag" data-dimension25="$99">Epomaker HE65 Mag</a> is an extremely powerful keyboard, especially for its size. Not only does it have per-key and quad-actuation, Rapid Trigger and Snap Tap like pricier keyboards, it also features 8,000Hz polling over a 2.4GHz wireless connection. The keyboard is easy to customize via its companion software, and as long as you don’t mind switch choice being limited, it’s well worth the money.<a class="view-deal button" href="http://amazon.com/EPOMAKER-HE65-Wireless-Actuation-Adjustable-Gasket-Mount/dp/B0DTJX16FY/" target="_blank" rel="nofollow" data-dimension112="797dea9f-7273-486c-a79b-953a8604c593" data-action="Deal Block" data-label="HE65 Mag" data-dimension48="HE65 Mag" data-dimension25="$99">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/computing/peripherals/7-things-to-know-when-choosing-your-first-mechanical-keyboard">7 things to know when choosing your first mechanical keyboard</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/i-reviewed-a-usd230-usd120-and-usd80-mechanical-keyboard-this-week-this-is-the-only-one-id-buy">I reviewed a $230, $120 and $80 mechanical keyboard this week — this is the only one I’d buy</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Stop spending $250 on mechanical keyboards that suffer from significant flaws — here’s why the Corsair Vanguard Air 99 Wireless doesn’t earn its keep ]]></title>
                                                                                                <dc:content><![CDATA[ <p>"How much should you spend on a good keyboard?" That's the question I've been trying to answer for the last two years while reviewing them. Those who aren't well-versed with the vast world of keyboards may automatically assume that just because a deck costs over $100, that means it's "worth it." But as someone who tests the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards</a> for a living, that's not necessarily true.</p><p>Personally, I don't think you should be spending more than $150 on a keeb — and even that's stretching it quite a bit. You see, in recent years, technology has come leaps and bounds, and you're getting manufacturers like Epomaker making super cheap Hall Effect keyboards (like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">$59 HE68</a>). So when a brand like Corsair comes out with a keyboard that costs $259, needless to say, I'm instantly skeptical.</p><div class="product"><a data-dimension112="3513e4de-532d-4a69-9521-4d1d726025f6" data-action="Deal Block" data-label="Vanguard Air 99 Wireless" data-dimension48="Vanguard Air 99 Wireless" data-dimension25="$259" href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" 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:32.40%;"><img id="ndtnjbeV4kb248ErMqS36Y" name="CV-Air-99-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ndtnjbeV4kb248ErMqS36Y-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="486" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow" data-dimension112="3513e4de-532d-4a69-9521-4d1d726025f6" data-action="Deal Block" data-label="Vanguard Air 99 Wireless" data-dimension48="Vanguard Air 99 Wireless" data-dimension25="$259">Vanguard Air 99 Wireless: $259</a></strong><br>Featuring 8,000Hz polling and N-key rollover, the Air 99 Wireless is a gaming powerhouse, and it’s compatible with the PS5 and Xbox too. Intuitive web-based software makes customization super easy too. But the screen could be more useful, the battery life could be better, and Stream Deck compatibility could be more refined. Oh, and let’s not forget about that $259 price tag!<a class="view-deal button" href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow" data-dimension112="3513e4de-532d-4a69-9521-4d1d726025f6" data-action="Deal Block" data-label="Vanguard Air 99 Wireless" data-dimension48="Vanguard Air 99 Wireless" data-dimension25="$259">View Deal</a></p></div><p>I, of course, didn't judge the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> harshly simply because it's expensive (that would be very shortsighted of me) — it's just that it doesn't do <em>enough </em>to justify the steep price. There are plenty of things to love about the Air 99 Wireless, but equally, there are a few things that leave you feeling frustrated. Here's why I awarded it 3-stars.</p><h2 id="it-gets-a-few-things-right">It gets a few things 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="wa5on3V8sR6LLo8j6VtTBk" name="Vanguard_Air_99_Wireless_keyboard_005.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/wa5on3V8sR6LLo8j6VtTBk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 won't be a Grinch and I'll start with the good things first, and what the Corsair Vanguard Air 99 Wireless gets incredibly right. Let's start with the design and build. The Air 99 Wireless is designed beautifully, and it's fitted with a robust aluminum frame which ensures the keyboard will stand the test of time. Double-shot PBT keycaps, a bright LCD screen and customizable RGB lighting make it a sight for sore eyes.</p><p>The Air 99 Wireless' pre-lubed OPX low-profile switches are an utter delight and don't feel fatiguing after a full day's worth of work and gaming. They have a short pre-travel distance of 1.5mm and require 45g of force to activate. The 1.5mm distance means you don't need to press them all the way down for the keyboard to register your commands, letting you type quickly.</p><p>Another thing I love is the Air 99 Wireless' gaming performance, bolstered by N-key rollover, 8,000Hz polling, and FlashTap (Last Key Prioritization). 8KHz polling is a little gimmicky and 1KHz is more than enough for 99% of players, so it's worth dropping the polling rate down to save the battery and not drain system resources. Peerless performance, really, especially in the realm of mechanical keyboards.</p><h2 id="where-it-all-falls-apart">Where it all falls apart</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uQPVaTxSVNMJiww34t7xoe" name="Vanguard_Air_99_Wireless_keyboard_011.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/uQPVaTxSVNMJiww34t7xoe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Regardless of how much I love the Corsair Vanguard Air 99 Wireless' switches, they don't do enough to overshadow the keyboard's flaws — and there are a few, and I'll kick things off with the battery life. In one word, the battery life is... underwhelming. Corsair says that the longest the keyboard can last is 55 hours (with RGB lighting off over Bluetooth). If you have RGB lighting on and are using the screen, your mileage drops to 29 hours.</p><p>For the price you're paying, that just won't do. When a super budget keyboard like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> can last a whopping 600 hours, what excuse does a $259 keyboard have? The battery life is, of course, impacted by the LCD screen, which I'm not a huge fan of. Sure, it's nice to be able to display a GIF as I did, but I wish the screen was more useful and didn't require a bunch of key combos to change settings.</p><p>Lastly, Stream Deck compatibility. The Air 99 Wireless features six programmable SD keys powered by the Elgato Stream Deck app, and in theory, these are great and give you quick access to apps and actions to streamline your workflow. But in practice, I found Stream Deck compatibility to be finicky at best. The app would fail to recognize the keyboard, or it would forget it when I plugged it back in the second time. More than anything, it just frustrated me.</p><h2 id="my-wallet-is-crying">My wallet is crying</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="i2peh6WTon9TmRQkE3gZYj" name="Vanguard_Air_99_Wireless_keyboard_007.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/i2peh6WTon9TmRQkE3gZYj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Poor battery life, a frustrating screen, and finicky Stream Deck compatibility, all contribute towards the Corsair Vanguard Air 99 Wireless struggling to justify its price tag. The Air 99 Wireless retails for <a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/">£239 at Amazon</a>, which makes my eyes squint uncomfortably, and is sure to make some people's wallets cry.</p><p>This doesn't mean that the keyboard is <em>all </em>bad. According to <a href="https://www.tomsguide.com/reference/about-us#section-how-we-test-and-rate-products" target="_blank" rel="nofollow">our rating guide</a>, a 3-star rating is defined as, "Good but there are better options." And the Air 99 Wireless features fierce competition — from Keychron, Lemokey, Epomaker, you name it.</p><p>Today, more advanced Hall Effect keyboards cost a fraction of Corsair's mechanical keyboard, and mech decks themselves easily come under $100. At the end of the day, the Air 99 Wireless easily gets lost in a crowded market — and fails to make a lasting impression.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xm4P0O"></div>                            </div>                            <script src="https://kwizly.com/embed/Xm4P0O.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/computing/keyboards/corsair-vanguard-air-99-wireless-review">The Corsair Vanguard Air 99 Wireless is a low-profile mechanical keyboard that costs a whopping $259 — I tested it to find out if it’s worth the premium</a></li><li><a href="https://www.tomsguide.com/audio/headphones/gaming-headphones-are-finally-good-for-listening-to-music-these-open-backs-prove-that-you-dont-need-two-separate-sets-of-cans">Gaming headphones are finally good for listening to music — these open-backs prove that you don’t need two separate sets of cans</a></li><li><a href="https://www.tomsguide.com/gaming/playstation/i-love-my-ps5-more-than-any-other-gadget-i-own-and-these-6-controllers-take-my-gaming-to-the-next-level">I love my PS5 more than any other gadget I own, and these 6 controllers take my gaming to the next level</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/stop-spending-usd250-on-mechanical-keyboards-that-suffer-from-significant-flaws-heres-why-the-corsair-vanguard-air-99-wireless-doesnt-earn-its-keep</link>
                                                                            <description>
                            <![CDATA[ Personally, I don’t believe any keyboard should cost more than $150, so when I tested the Corsair Vanguard Air 99 Wireless, I was left feeling shortchanged. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">YtvYaQxqCzPNwNYFyJJw7d</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/VJsHC2WwZSxVMkvdz4syQj-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 09 May 2026 07:00:00 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/VJsHC2WwZSxVMkvdz4syQj-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/VJsHC2WwZSxVMkvdz4syQj-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>"How much should you spend on a good keyboard?" That's the question I've been trying to answer for the last two years while reviewing them. Those who aren't well-versed with the vast world of keyboards may automatically assume that just because a deck costs over $100, that means it's "worth it." But as someone who tests the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards</a> for a living, that's not necessarily true.</p><p>Personally, I don't think you should be spending more than $150 on a keeb — and even that's stretching it quite a bit. You see, in recent years, technology has come leaps and bounds, and you're getting manufacturers like Epomaker making super cheap Hall Effect keyboards (like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">$59 HE68</a>). So when a brand like Corsair comes out with a keyboard that costs $259, needless to say, I'm instantly skeptical.</p><div class="product"><a data-dimension112="3513e4de-532d-4a69-9521-4d1d726025f6" data-action="Deal Block" data-label="Vanguard Air 99 Wireless" data-dimension48="Vanguard Air 99 Wireless" data-dimension25="$259" href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" 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:32.40%;"><img id="ndtnjbeV4kb248ErMqS36Y" name="CV-Air-99-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/ndtnjbeV4kb248ErMqS36Y-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="486" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p><strong><a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow" data-dimension112="3513e4de-532d-4a69-9521-4d1d726025f6" data-action="Deal Block" data-label="Vanguard Air 99 Wireless" data-dimension48="Vanguard Air 99 Wireless" data-dimension25="$259">Vanguard Air 99 Wireless: $259</a></strong><br>Featuring 8,000Hz polling and N-key rollover, the Air 99 Wireless is a gaming powerhouse, and it’s compatible with the PS5 and Xbox too. Intuitive web-based software makes customization super easy too. But the screen could be more useful, the battery life could be better, and Stream Deck compatibility could be more refined. Oh, and let’s not forget about that $259 price tag!<a class="view-deal button" href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow" data-dimension112="3513e4de-532d-4a69-9521-4d1d726025f6" data-action="Deal Block" data-label="Vanguard Air 99 Wireless" data-dimension48="Vanguard Air 99 Wireless" data-dimension25="$259">View Deal</a></p></div><p>I, of course, didn't judge the <a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a> harshly simply because it's expensive (that would be very shortsighted of me) — it's just that it doesn't do <em>enough </em>to justify the steep price. There are plenty of things to love about the Air 99 Wireless, but equally, there are a few things that leave you feeling frustrated. Here's why I awarded it 3-stars.</p><h2 id="it-gets-a-few-things-right">It gets a few things 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="wa5on3V8sR6LLo8j6VtTBk" name="Vanguard_Air_99_Wireless_keyboard_005.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/wa5on3V8sR6LLo8j6VtTBk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 won't be a Grinch and I'll start with the good things first, and what the Corsair Vanguard Air 99 Wireless gets incredibly right. Let's start with the design and build. The Air 99 Wireless is designed beautifully, and it's fitted with a robust aluminum frame which ensures the keyboard will stand the test of time. Double-shot PBT keycaps, a bright LCD screen and customizable RGB lighting make it a sight for sore eyes.</p><p>The Air 99 Wireless' pre-lubed OPX low-profile switches are an utter delight and don't feel fatiguing after a full day's worth of work and gaming. They have a short pre-travel distance of 1.5mm and require 45g of force to activate. The 1.5mm distance means you don't need to press them all the way down for the keyboard to register your commands, letting you type quickly.</p><p>Another thing I love is the Air 99 Wireless' gaming performance, bolstered by N-key rollover, 8,000Hz polling, and FlashTap (Last Key Prioritization). 8KHz polling is a little gimmicky and 1KHz is more than enough for 99% of players, so it's worth dropping the polling rate down to save the battery and not drain system resources. Peerless performance, really, especially in the realm of mechanical keyboards.</p><h2 id="where-it-all-falls-apart">Where it all falls apart</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uQPVaTxSVNMJiww34t7xoe" name="Vanguard_Air_99_Wireless_keyboard_011.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/uQPVaTxSVNMJiww34t7xoe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Regardless of how much I love the Corsair Vanguard Air 99 Wireless' switches, they don't do enough to overshadow the keyboard's flaws — and there are a few, and I'll kick things off with the battery life. In one word, the battery life is... underwhelming. Corsair says that the longest the keyboard can last is 55 hours (with RGB lighting off over Bluetooth). If you have RGB lighting on and are using the screen, your mileage drops to 29 hours.</p><p>For the price you're paying, that just won't do. When a super budget keyboard like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he65-mag-review">Epomaker HE65 Mag</a> can last a whopping 600 hours, what excuse does a $259 keyboard have? The battery life is, of course, impacted by the LCD screen, which I'm not a huge fan of. Sure, it's nice to be able to display a GIF as I did, but I wish the screen was more useful and didn't require a bunch of key combos to change settings.</p><p>Lastly, Stream Deck compatibility. The Air 99 Wireless features six programmable SD keys powered by the Elgato Stream Deck app, and in theory, these are great and give you quick access to apps and actions to streamline your workflow. But in practice, I found Stream Deck compatibility to be finicky at best. The app would fail to recognize the keyboard, or it would forget it when I plugged it back in the second time. More than anything, it just frustrated me.</p><h2 id="my-wallet-is-crying">My wallet is crying</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="i2peh6WTon9TmRQkE3gZYj" name="Vanguard_Air_99_Wireless_keyboard_007.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/i2peh6WTon9TmRQkE3gZYj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Poor battery life, a frustrating screen, and finicky Stream Deck compatibility, all contribute towards the Corsair Vanguard Air 99 Wireless struggling to justify its price tag. The Air 99 Wireless retails for <a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/">£239 at Amazon</a>, which makes my eyes squint uncomfortably, and is sure to make some people's wallets cry.</p><p>This doesn't mean that the keyboard is <em>all </em>bad. According to <a href="https://www.tomsguide.com/reference/about-us#section-how-we-test-and-rate-products" target="_blank" rel="nofollow">our rating guide</a>, a 3-star rating is defined as, "Good but there are better options." And the Air 99 Wireless features fierce competition — from Keychron, Lemokey, Epomaker, you name it.</p><p>Today, more advanced Hall Effect keyboards cost a fraction of Corsair's mechanical keyboard, and mech decks themselves easily come under $100. At the end of the day, the Air 99 Wireless easily gets lost in a crowded market — and fails to make a lasting impression.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-Xm4P0O"></div>                            </div>                            <script src="https://kwizly.com/embed/Xm4P0O.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/computing/keyboards/corsair-vanguard-air-99-wireless-review">The Corsair Vanguard Air 99 Wireless is a low-profile mechanical keyboard that costs a whopping $259 — I tested it to find out if it’s worth the premium</a></li><li><a href="https://www.tomsguide.com/audio/headphones/gaming-headphones-are-finally-good-for-listening-to-music-these-open-backs-prove-that-you-dont-need-two-separate-sets-of-cans">Gaming headphones are finally good for listening to music — these open-backs prove that you don’t need two separate sets of cans</a></li><li><a href="https://www.tomsguide.com/gaming/playstation/i-love-my-ps5-more-than-any-other-gadget-i-own-and-these-6-controllers-take-my-gaming-to-the-next-level">I love my PS5 more than any other gadget I own, and these 6 controllers take my gaming to the next level</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ ‘One of the only budget mechanical keyboards I’d buy’: here’s how the Keychron V1 8K sets a new gold standard for cheap decks ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Way, way back in May 2024, I reviewed the <a href="https://www.tomsguide.com/computing/peripherals/keychron-v1-review">Keychron V1</a> and called it a faultless keyboard. Since then, Keychron has launched the Ultra lineup of mechanical keyboards, including the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a>, with 8,000Hz polling. And now, Keychron has packed 8KHz polling into my favorite wired keyboard — and the Keychron V1 8K is a delight.</p><p>Sporting great build quality and a low-key design, the V1 8K doesn’t feel like a budget keyboard. It looks premium and offers a beautiful typing experience. The combination of per-key and mix RGB lighting adds stunning pops of color to the keyboard, and the web-based Keychron Launcher makes customization a walk in the park.</p><p>Is this the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> for you? Read my full Keychron V1 8K review to find out.</p><h2 class="article-body__section" id="section-keychron-v1-8k-review-specs"><span>Keychron V1 8K 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/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow">$74</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Keychron Super Red (linear), Banana (tactile) or Brown (tactile) </p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 82 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB and Mix RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>1</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.91 x 5.81 x 1.04 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.19lbs</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-keychron-v1-8k-review-cheat-sheet"><span>Keychron V1 8K review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A 75% mechanical keyboard with 8,000Hz polling, and the successor to the Keychron V1</li><li><strong>Who is it for? </strong>Anyone who wants an affordable and lovely keyboard to type on</li><li><strong>How much does it cost? </strong>Not a lot! The Keychron V1 8K is available for <a href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow">$74</a></li><li><strong>What do we like?</strong> The great build quality, typing experience, extensive RGB customization, affordable price, and web-based software</li><li><strong>What don’t we like? </strong>There’s some flex in the body, and there’s no wireless variant</li></ul><h2 class="article-body__section" id="section-keychron-v1-8k-review-the-ups"><span>Keychron V1 8K review: The ups</span></h2><p>From its great build quality and the outstanding typing experience it offers, to its web-based software and affordable price tag, the Keychron V1 8K impresses on many fronts.</p><h2 id="top-notch-build">Top-notch build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TxMJ2hWZifRUJRWJ335Nfc" name="Keychron_V1_8K_keyboard_003.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/TxMJ2hWZifRUJRWJ335Nfc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 there’s one thing I’ve learned after having tested many, <em>many</em> Keychron keyboards, it’s that the brand’s decks are usually very well-built. The Keychron V1 8K is no different. Just like its 1KHz counterpart, the V1 8K is made entirely of ABS plastic, but this plastic doesn’t feel cheap or look tacky. It feels high quality and tapping the keyboard with my fingernails didn’t reveal any hollow-sounding parts.</p><p>Because the keyboard is made of plastic, it isn’t very heavy either, making it a great choice for those who travel a lot. The V1 8K weighs 2.19lbs, so it’s a little heavier than the V1 (2.13lbs) and the V1 Ultra 8K (1.69lbs) but you can barely tell the difference between the three. I’ve had no issues carrying the V1 8K to and from work in my backpack — even when my bag was already stuffed to the brim with other gadgets.</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:1622px;"><p class="vanilla-image-block" style="padding-top:66.58%;"><img id="khpfgsQmYTndNeCNERC2Vb" name="Keychron_V1_8K_keyboard_007.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/khpfgsQmYTndNeCNERC2Vb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1622" height="1080" attribution="" endorsement="" class="inline"></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, the V1 8K doesn’t take up too much space in bags or on your desk, as it measures 12.91 x 5.81 x 1.04 inches. It sports a 75% layout with 82 keys and a metal knob — which adds to the premium build of the keyboard— and now features an orange dot for a pop of color. Also adding to the V1 8K’s top-notch build is its use of double-shot PBT keycaps, which are more durable and resistant to wear and tear compared to their ABS counterparts.</p><p>Aiding the V1 8K’s structural integrity is the gasket-mount design which offers a cushioned typing feel and reduces vibrations from key presses. Furthermore, Keychron has added two extra layers of sound-absorbing foam to the V1 8K — the original used three. These work in conjunction with the gasket mount to deliver a lovely sound profile (more on that shortly).</p><h2 id="beautiful-rgb-lighting">Beautiful RGB lighting</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Hh7tA9G3h5BJ6ZjP5GnCec" name="Keychron_V1_8K_keyboard_002.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Hh7tA9G3h5BJ6ZjP5GnCec-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 OG Keychron V1 already looked stunning with its per-key RGB, but Keychron has taken it up a level by adding per-key RGB and “mix RGB” to the Keychron V1 8K. Mix RGB was first introduced on the Keychron V1 Ultra 8K, and it’s nice to see it make an appearance on a much cheaper keyboard. Each key’s lighting can be individually customized, and over 22 effects can be applied, thanks to per-key RGB.</p><p>Mix RGB, on the other hand, splits the keyboard into two custom lighting zones, each with its own unique effects. Basically, you can set different lighting effects to different keys via <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher</a>. As I did on the V1 Ultra 8K, I set the alphabet keys to reactive lighting so they only light up when pressed, and all other keys to remain lit up all the time in a static effect. If you like colorful keyboards, the V1 8K is the perfect choice for you.</p><h2 id="excellent-for-typing-2">Excellent for typing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5R7CZ7jFBqSKTdiimUGxLb" name="Keychron_V1_8K_keyboard_010.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/5R7CZ7jFBqSKTdiimUGxLb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 three keyboards in the V1 lineup utilize different mechanical switches. The OG V1 came with Keychron K Pro switches; the V1 Ultra 8K packs Keychron Silk POM ones; and the V1 8K features Keychron Super switches. The keyboard is hot-swappable, so you can use any compatible 3- or 5-pin switches with it without having to de- and re-solder them to the PCB. As for stock ones, you can choose between Red (linear), Banana (tactile) or Brown (tactile).</p><p>I tested the Banana tactiles which are a little on the heavier side of things as they require 57g of force to activate. They have a pre-travel distance of 2mm and a total travel distance of 3.4mm, so you don’t need to press them all the way down to activate them. I personally prefer lighter, linear switches, like the 35g Sea Salts in the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115) or even the 50g White Rains in the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129).</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="WzPgykDHCmgLkk6WV5Bodb" name="Keychron_V1_8K_keyboard_009.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/WzPgykDHCmgLkk6WV5Bodb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Regardless, I didn’t find the Bananas too fatiguing to type on — I typed on them for seven hours straight without feeling any discomfort! The V1 8K sounds fantastic too, thanks to the aforementioned several layers of sound-dampening foam. The keyboard has a satisfying sound profile, a little clacky but also slightly thocky — enough for you to enjoy it without disturbing others around you.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Keychron V1 8K</p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.5</p></td><td  ><p>91.2%</p></td></tr></tbody></table></div><p>As I do with all keyboards I review, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> to see how the V1 8K fared against other mech decks I’d tested. As you can see in the table above, I achieved 99WPM with 95.03% accuracy, which brought up my average score. I’m very happy with this.</p><h2 id="now-with-8khz-polling">Now with 8KHz polling</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="mUy7hSqAAjbEjswGzy9jrb" name="Keychron_V1_8K_keyboard_006.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/mUy7hSqAAjbEjswGzy9jrb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 seems like all keyboard manufacturers like adding 8,000Hz polling to their decks. It’s not surprising, of course, because that’s the buzzword these days, but most players won’t be able to tell the difference between 8,000Hz and 1,000Hz polling. Both polling rates reduce or even negate latency issues and lag, and they ensure that you don’t experience any missed inputs.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>While the V1 8K is primarily a productivity-first keyboard, I played a bit of <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077 </a>and Counter-Strike 2 to see how the board fared. Sure enough, I didn’t experience any input lag or unresponsiveness thanks to 8,000Hz polling. But I also didn’t notice any when playing at 1,000Hz polling. 8KHz polling can be a massive drain on system resources, so if you’re worried about that, I’d recommend dropping the polling (it can be adjusted down to 125Hz) via Keychron Launcher.</p><p>The V1 8K’s Banana switches are highly responsive too, as all my commands were registered instantaneously. Though if you’re getting this keyboard for gaming, I’d recommend grabbing the Super Red linears instead, as they have an actuation force of 45g, so they’re a little lighter (and less fatiguing). While the V1 8K is suited to casual gaming, it’s worth investing in a Hall Effect keyboard, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> ($59) or the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a> ($99). If you’re a competitive gamer, then magnetic keyboards have other useful features, like adjustable actuation and Last-Key Prioritization.</p><h2 id="web-based-companion-software-3">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/xx2tc7jiHisZhNmbVLS2Z-1920-80.jpg" alt="Keychron Launcher" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/EXrppAEXsLFk8QytLZkmZ-1920-80.jpg" alt="Keychron Launcher" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/zzXodz56Bvij4YQttaCqY-1920-80.jpg" alt="Keychron Launcher" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure></figure><p>Similar to most other Keychron keyboards, the Keychron V1 8K doesn’t require you to download third-party software or bloatware for customization. Instead, it uses the web-based <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher</a>, so all you need to do is plug the keyboard in and open the website in your browser.</p><p>Keychron Launcher sports a clean interface and it’s extremely user-friendly, and it gives you quick access to the V1 8K’s settings. You can use Keychron Launcher to remap the V1 8K’s keys, personalize the per-key and mix RGB, record macros, and adjust the polling rate between 125Hz and 8,000Hz. Easy as pie!</p><h2 id="still-affordable">Still affordable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dGa7KRyC6e3v74CakfoCHc" name="Keychron_V1_8K_keyboard_014.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/dGa7KRyC6e3v74CakfoCHc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 best thing about the Keychron V1 8K is just how affordable it is. When the OG V1 was launched, it had a retail price of $99 (you can get it for around $70 these days). The V1 Ultra 8K, on the other hand, has a retail price of $114. Priced at just <a href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow">$74</a>, the V1 8K is cheap and very cheerful, and it’s extremely easy to recommend the board at this price.</p><p>We’ve seen some excellent mechanical keyboards in this price range, like the <a href="https://www.tomsguide.com/computing/peripherals/wobkey-rainy-75-pro-review">Wobkey Rainy75 Lite</a> ($99), the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-dynatab75-review">Epomaker DynaTab75</a> ($89) and the <a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">McHose X75 V2</a> ($89). But none of these are as cheap as the V1 8K — and none offer 8KHz polling either, or extensive RGB customization.</p><h2 class="article-body__section" id="section-keychron-v1-8k-review-the-downs"><span>Keychron V1 8K review: The downs</span></h2><p>The Keychron V1 8K doesn’t have many flaws, but it’s worth noting that there’s some flex in the body, and it’s a wired-only keyboard.</p><h2 id="wired-only-3">Wired only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="y4QvRiy47ri7SkWZZVYtmc" name="Keychron_V1_8K_keyboard_012.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/y4QvRiy47ri7SkWZZVYtmc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 more of a pre-purchase condition than an outright flaw, but the Keychron V1 8K is a wired-only keyboard. At the time of writing, there’s no Max (wireless) version, but this might change in the future and if it does, I’ll update this review. The OG Keychron V1 Max is still available, and you can usually find it for around <a href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR14LGX6/" target="_blank" rel="nofollow">$84 during sales events</a>.</p><p>If you want a wireless but affordable keyboard, it’s worth checking out the similarly priced <a href="https://www.tomsguide.com/computing/peripherals/gravastar-mercury-k1-lite-review">GravaStar Mercury K1 Lite</a> ($89) or the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-kick75-review">NuPhy Kick75</a> ($89). You’ll be missing out on 8KHz polling but, as I mentioned earlier, I don’t think it’s a significant loss.</p><h2 id="some-flex">Some flex</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VrXSLhqBptzeLuLqXXGCCc" name="Keychron_V1_8K_keyboard_005.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/VrXSLhqBptzeLuLqXXGCCc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 only real drawback of the Keychron V1 8K is that there is some flex in its body, especially when you hold it by its sides and twist it. This is quite disappointing as other than this, the V1 8K is excellently built. I didn’t experience this with the OG V1, and it’s because the keyboard’s predecessor featured a steel plate. I’m not entirely sure why Keychron decided not to implement a steel plate in the V1 8K — perhaps to keep the costs down.</p><p>This is also something I’ve experienced with many other all-plastic keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th99-review">Epomaker TH99</a> ($82). If this is something that bothers you and you still want a plastic keyboard but don’t mind jumping to an 80% layout, the <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review">8BitDo Retro</a> ($99) is a superb alternative.</p><h2 class="article-body__section" id="section-keychron-v1-8k-review-verdict"><span>Keychron V1 8K 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="YFWpZMsU3paCoayyYgWKmc" name="Keychron_V1_8K_keyboard_004.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/YFWpZMsU3paCoayyYgWKmc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Three things in life are certain: death, taxes, and most Keychron keyboards being outstanding. The Keychron V1 8K is a lovely little mechanical keyboard, packing responsive switches for typing and 8,000Hz polling for gaming. This is a great-looking board, thanks to its combination of per-key and mix RGB, and customizing said lighting is a walk in the park, as the web-based Keychron Launcher is a delight to use.</p><p>But there’s no wireless variant of the V1 8K, and the keyboard has some flex in its body. Are either of those dealbreakers? Not even a little bit. The V1 8K costs less than $80, and for that budget price, you’re getting a powerhouse for productivity and 8KHz polling, which you don’t see on many cheap or mechanical keyboards. For budget keyboard enthusiasts and hardcore typists, there are few decks better than the V1 8K.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/keychron-v1-8k-review</link>
                                                                            <description>
                            <![CDATA[ The Keychron V1 8K is a beautiful budget mechanical keyboard, offering a lovely typing experience and extensively customizable RGB lighting. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">bknNeWTmtaCMgCciWZckJU</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/jjAPDyM6vSyzwb9axF2SAc-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 08 May 2026 08:50:29 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:34 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/jjAPDyM6vSyzwb9axF2SAc-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A Keychron V1 8K wired mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A Keychron V1 8K wired mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A Keychron V1 8K wired mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/jjAPDyM6vSyzwb9axF2SAc-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Way, way back in May 2024, I reviewed the <a href="https://www.tomsguide.com/computing/peripherals/keychron-v1-review">Keychron V1</a> and called it a faultless keyboard. Since then, Keychron has launched the Ultra lineup of mechanical keyboards, including the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a>, with 8,000Hz polling. And now, Keychron has packed 8KHz polling into my favorite wired keyboard — and the Keychron V1 8K is a delight.</p><p>Sporting great build quality and a low-key design, the V1 8K doesn’t feel like a budget keyboard. It looks premium and offers a beautiful typing experience. The combination of per-key and mix RGB lighting adds stunning pops of color to the keyboard, and the web-based Keychron Launcher makes customization a walk in the park.</p><p>Is this the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboard</a> for you? Read my full Keychron V1 8K review to find out.</p><h2 class="article-body__section" id="section-keychron-v1-8k-review-specs"><span>Keychron V1 8K 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/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow">$74</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Keychron Super Red (linear), Banana (tactile) or Brown (tactile) </p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 82 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB and Mix RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>1</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.91 x 5.81 x 1.04 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.19lbs</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-keychron-v1-8k-review-cheat-sheet"><span>Keychron V1 8K review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A 75% mechanical keyboard with 8,000Hz polling, and the successor to the Keychron V1</li><li><strong>Who is it for? </strong>Anyone who wants an affordable and lovely keyboard to type on</li><li><strong>How much does it cost? </strong>Not a lot! The Keychron V1 8K is available for <a href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow">$74</a></li><li><strong>What do we like?</strong> The great build quality, typing experience, extensive RGB customization, affordable price, and web-based software</li><li><strong>What don’t we like? </strong>There’s some flex in the body, and there’s no wireless variant</li></ul><h2 class="article-body__section" id="section-keychron-v1-8k-review-the-ups"><span>Keychron V1 8K review: The ups</span></h2><p>From its great build quality and the outstanding typing experience it offers, to its web-based software and affordable price tag, the Keychron V1 8K impresses on many fronts.</p><h2 id="top-notch-build">Top-notch build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TxMJ2hWZifRUJRWJ335Nfc" name="Keychron_V1_8K_keyboard_003.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/TxMJ2hWZifRUJRWJ335Nfc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 there’s one thing I’ve learned after having tested many, <em>many</em> Keychron keyboards, it’s that the brand’s decks are usually very well-built. The Keychron V1 8K is no different. Just like its 1KHz counterpart, the V1 8K is made entirely of ABS plastic, but this plastic doesn’t feel cheap or look tacky. It feels high quality and tapping the keyboard with my fingernails didn’t reveal any hollow-sounding parts.</p><p>Because the keyboard is made of plastic, it isn’t very heavy either, making it a great choice for those who travel a lot. The V1 8K weighs 2.19lbs, so it’s a little heavier than the V1 (2.13lbs) and the V1 Ultra 8K (1.69lbs) but you can barely tell the difference between the three. I’ve had no issues carrying the V1 8K to and from work in my backpack — even when my bag was already stuffed to the brim with other gadgets.</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:1622px;"><p class="vanilla-image-block" style="padding-top:66.58%;"><img id="khpfgsQmYTndNeCNERC2Vb" name="Keychron_V1_8K_keyboard_007.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/khpfgsQmYTndNeCNERC2Vb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1622" height="1080" attribution="" endorsement="" class="inline"></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, the V1 8K doesn’t take up too much space in bags or on your desk, as it measures 12.91 x 5.81 x 1.04 inches. It sports a 75% layout with 82 keys and a metal knob — which adds to the premium build of the keyboard— and now features an orange dot for a pop of color. Also adding to the V1 8K’s top-notch build is its use of double-shot PBT keycaps, which are more durable and resistant to wear and tear compared to their ABS counterparts.</p><p>Aiding the V1 8K’s structural integrity is the gasket-mount design which offers a cushioned typing feel and reduces vibrations from key presses. Furthermore, Keychron has added two extra layers of sound-absorbing foam to the V1 8K — the original used three. These work in conjunction with the gasket mount to deliver a lovely sound profile (more on that shortly).</p><h2 id="beautiful-rgb-lighting">Beautiful RGB lighting</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Hh7tA9G3h5BJ6ZjP5GnCec" name="Keychron_V1_8K_keyboard_002.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Hh7tA9G3h5BJ6ZjP5GnCec-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 OG Keychron V1 already looked stunning with its per-key RGB, but Keychron has taken it up a level by adding per-key RGB and “mix RGB” to the Keychron V1 8K. Mix RGB was first introduced on the Keychron V1 Ultra 8K, and it’s nice to see it make an appearance on a much cheaper keyboard. Each key’s lighting can be individually customized, and over 22 effects can be applied, thanks to per-key RGB.</p><p>Mix RGB, on the other hand, splits the keyboard into two custom lighting zones, each with its own unique effects. Basically, you can set different lighting effects to different keys via <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher</a>. As I did on the V1 Ultra 8K, I set the alphabet keys to reactive lighting so they only light up when pressed, and all other keys to remain lit up all the time in a static effect. If you like colorful keyboards, the V1 8K is the perfect choice for you.</p><h2 id="excellent-for-typing-2">Excellent for typing</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="5R7CZ7jFBqSKTdiimUGxLb" name="Keychron_V1_8K_keyboard_010.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/5R7CZ7jFBqSKTdiimUGxLb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 three keyboards in the V1 lineup utilize different mechanical switches. The OG V1 came with Keychron K Pro switches; the V1 Ultra 8K packs Keychron Silk POM ones; and the V1 8K features Keychron Super switches. The keyboard is hot-swappable, so you can use any compatible 3- or 5-pin switches with it without having to de- and re-solder them to the PCB. As for stock ones, you can choose between Red (linear), Banana (tactile) or Brown (tactile).</p><p>I tested the Banana tactiles which are a little on the heavier side of things as they require 57g of force to activate. They have a pre-travel distance of 2mm and a total travel distance of 3.4mm, so you don’t need to press them all the way down to activate them. I personally prefer lighter, linear switches, like the 35g Sea Salts in the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115) or even the 50g White Rains in the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129).</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="WzPgykDHCmgLkk6WV5Bodb" name="Keychron_V1_8K_keyboard_009.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/WzPgykDHCmgLkk6WV5Bodb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Regardless, I didn’t find the Bananas too fatiguing to type on — I typed on them for seven hours straight without feeling any discomfort! The V1 8K sounds fantastic too, thanks to the aforementioned several layers of sound-dampening foam. The keyboard has a satisfying sound profile, a little clacky but also slightly thocky — enough for you to enjoy it without disturbing others around you.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Keychron V1 8K</p></td><td  ><p>99</p></td><td  ><p>95.03%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review">Corsair Vanguard Air 99 Wireless</a></p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.5</p></td><td  ><p>91.2%</p></td></tr></tbody></table></div><p>As I do with all keyboards I review, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> to see how the V1 8K fared against other mech decks I’d tested. As you can see in the table above, I achieved 99WPM with 95.03% accuracy, which brought up my average score. I’m very happy with this.</p><h2 id="now-with-8khz-polling">Now with 8KHz polling</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="mUy7hSqAAjbEjswGzy9jrb" name="Keychron_V1_8K_keyboard_006.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/mUy7hSqAAjbEjswGzy9jrb-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 seems like all keyboard manufacturers like adding 8,000Hz polling to their decks. It’s not surprising, of course, because that’s the buzzword these days, but most players won’t be able to tell the difference between 8,000Hz and 1,000Hz polling. Both polling rates reduce or even negate latency issues and lag, and they ensure that you don’t experience any missed inputs.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>While the V1 8K is primarily a productivity-first keyboard, I played a bit of <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077 </a>and Counter-Strike 2 to see how the board fared. Sure enough, I didn’t experience any input lag or unresponsiveness thanks to 8,000Hz polling. But I also didn’t notice any when playing at 1,000Hz polling. 8KHz polling can be a massive drain on system resources, so if you’re worried about that, I’d recommend dropping the polling (it can be adjusted down to 125Hz) via Keychron Launcher.</p><p>The V1 8K’s Banana switches are highly responsive too, as all my commands were registered instantaneously. Though if you’re getting this keyboard for gaming, I’d recommend grabbing the Super Red linears instead, as they have an actuation force of 45g, so they’re a little lighter (and less fatiguing). While the V1 8K is suited to casual gaming, it’s worth investing in a Hall Effect keyboard, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-he68-hall-effect-keyboard-review">Epomaker HE68</a> ($59) or the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a> ($99). If you’re a competitive gamer, then magnetic keyboards have other useful features, like adjustable actuation and Last-Key Prioritization.</p><h2 id="web-based-companion-software-3">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/xx2tc7jiHisZhNmbVLS2Z-1920-80.jpg" alt="Keychron Launcher" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/EXrppAEXsLFk8QytLZkmZ-1920-80.jpg" alt="Keychron Launcher" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/zzXodz56Bvij4YQttaCqY-1920-80.jpg" alt="Keychron Launcher" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure></figure><p>Similar to most other Keychron keyboards, the Keychron V1 8K doesn’t require you to download third-party software or bloatware for customization. Instead, it uses the web-based <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher</a>, so all you need to do is plug the keyboard in and open the website in your browser.</p><p>Keychron Launcher sports a clean interface and it’s extremely user-friendly, and it gives you quick access to the V1 8K’s settings. You can use Keychron Launcher to remap the V1 8K’s keys, personalize the per-key and mix RGB, record macros, and adjust the polling rate between 125Hz and 8,000Hz. Easy as pie!</p><h2 id="still-affordable">Still affordable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="dGa7KRyC6e3v74CakfoCHc" name="Keychron_V1_8K_keyboard_014.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/dGa7KRyC6e3v74CakfoCHc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 best thing about the Keychron V1 8K is just how affordable it is. When the OG V1 was launched, it had a retail price of $99 (you can get it for around $70 these days). The V1 Ultra 8K, on the other hand, has a retail price of $114. Priced at just <a href="https://www.amazon.com/Keychron-Mechanical-Programmable-Hot-Swappable-Compatible/dp/B0G24KLD4V/" target="_blank" rel="nofollow">$74</a>, the V1 8K is cheap and very cheerful, and it’s extremely easy to recommend the board at this price.</p><p>We’ve seen some excellent mechanical keyboards in this price range, like the <a href="https://www.tomsguide.com/computing/peripherals/wobkey-rainy-75-pro-review">Wobkey Rainy75 Lite</a> ($99), the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-dynatab75-review">Epomaker DynaTab75</a> ($89) and the <a href="https://www.tomsguide.com/computing/peripherals/mchose-x75-v2-keyboard-review">McHose X75 V2</a> ($89). But none of these are as cheap as the V1 8K — and none offer 8KHz polling either, or extensive RGB customization.</p><h2 class="article-body__section" id="section-keychron-v1-8k-review-the-downs"><span>Keychron V1 8K review: The downs</span></h2><p>The Keychron V1 8K doesn’t have many flaws, but it’s worth noting that there’s some flex in the body, and it’s a wired-only keyboard.</p><h2 id="wired-only-3">Wired only</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="y4QvRiy47ri7SkWZZVYtmc" name="Keychron_V1_8K_keyboard_012.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/y4QvRiy47ri7SkWZZVYtmc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 more of a pre-purchase condition than an outright flaw, but the Keychron V1 8K is a wired-only keyboard. At the time of writing, there’s no Max (wireless) version, but this might change in the future and if it does, I’ll update this review. The OG Keychron V1 Max is still available, and you can usually find it for around <a href="https://www.amazon.com/Keychron-Mechanical-Bluetooth-Programmable-Hot-Swappable/dp/B0CR14LGX6/" target="_blank" rel="nofollow">$84 during sales events</a>.</p><p>If you want a wireless but affordable keyboard, it’s worth checking out the similarly priced <a href="https://www.tomsguide.com/computing/peripherals/gravastar-mercury-k1-lite-review">GravaStar Mercury K1 Lite</a> ($89) or the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-kick75-review">NuPhy Kick75</a> ($89). You’ll be missing out on 8KHz polling but, as I mentioned earlier, I don’t think it’s a significant loss.</p><h2 id="some-flex">Some flex</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VrXSLhqBptzeLuLqXXGCCc" name="Keychron_V1_8K_keyboard_005.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/VrXSLhqBptzeLuLqXXGCCc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 only real drawback of the Keychron V1 8K is that there is some flex in its body, especially when you hold it by its sides and twist it. This is quite disappointing as other than this, the V1 8K is excellently built. I didn’t experience this with the OG V1, and it’s because the keyboard’s predecessor featured a steel plate. I’m not entirely sure why Keychron decided not to implement a steel plate in the V1 8K — perhaps to keep the costs down.</p><p>This is also something I’ve experienced with many other all-plastic keyboards, like the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-th99-review">Epomaker TH99</a> ($82). If this is something that bothers you and you still want a plastic keyboard but don’t mind jumping to an 80% layout, the <a href="https://www.tomsguide.com/computing/peripherals/8bitdo-retro-mechanical-keyboard-review">8BitDo Retro</a> ($99) is a superb alternative.</p><h2 class="article-body__section" id="section-keychron-v1-8k-review-verdict"><span>Keychron V1 8K 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="YFWpZMsU3paCoayyYgWKmc" name="Keychron_V1_8K_keyboard_004.JPG" alt="A Keychron V1 8K wired mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/YFWpZMsU3paCoayyYgWKmc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Three things in life are certain: death, taxes, and most Keychron keyboards being outstanding. The Keychron V1 8K is a lovely little mechanical keyboard, packing responsive switches for typing and 8,000Hz polling for gaming. This is a great-looking board, thanks to its combination of per-key and mix RGB, and customizing said lighting is a walk in the park, as the web-based Keychron Launcher is a delight to use.</p><p>But there’s no wireless variant of the V1 8K, and the keyboard has some flex in its body. Are either of those dealbreakers? Not even a little bit. The V1 8K costs less than $80, and for that budget price, you’re getting a powerhouse for productivity and 8KHz polling, which you don’t see on many cheap or mechanical keyboards. For budget keyboard enthusiasts and hardcore typists, there are few decks better than the V1 8K.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The Corsair Vanguard Air 99 Wireless is a low-profile mechanical keyboard that costs a whopping $259 — I tested it to find out if it’s worth the premium ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> can take your performance to the next level by lending you a competitive advantage. Corsair is no stranger to making some fine gaming decks, and the Vanguard Air 99 Wireless comes close to being a formidable beast — but it falls just short. The Air 99 Wireless packs ultra responsive low-profile mechanical switches, 8,000Hz polling and N-key rollover, all of which work very well.</p><p>It’s a stunning keyboard to type on too, and it's also got the looks. The keyboard is accompanied by intuitive web-based companion software… but that’s about it. Unfortunately, the Air 99 Wireless seriously falters behind the competition due to subpar battery life, finicky Stream Deck compatibility, a lackluster screen, and a frankly outrageous price tag.</p><p>For the complete breakdown, read my full Corsair Vanguard Air 99 Wireless review.</p><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-specs"><span>Corsair Vanguard Air 99 Wireless 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/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/" target="_blank" rel="nofollow">£239</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Corsair OPX Low-Profile</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>99%, 108 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows, PS5, Xbox Series X, Xbox Series S</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable Lithium-ion polymer</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>16.76 x 5.42 x 0.7 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.98lbs</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-corsair-vanguard-air-99-wireless-review-cheat-sheet"><span>Corsair Vanguard Air 99 Wireless review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A low-profile mechanical keyboard with dedicated Stream Deck keys and 8,000Hz polling</li><li><strong>Who is it for?</strong> For competitive gamers and serious typists… with quite a bit of money</li><li><strong>How much does it cost? </strong>The Corsair Vanguard Air 99 Wireless is available for <a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/" target="_blank" rel="nofollow">£239</a></li><li><strong>What do we like? </strong>The stunning design and build, lovely switches for typing and gaming, versatile compatibility, and intuitive web-based software</li><li><strong>What don’t we like? </strong>The low-key useless screen, finicky Stream Deck support, poor battery life, and expensive price</li></ul><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-the-ups"><span>Corsair Vanguard Air 99 Wireless review: The ups</span></h2><p>The Corsair Vanguard Air 99 Wireless is a very good looking and well-built keyboard, and it boasts excellent gaming performance and a wonderful typing experience.</p><h2 id="stunning-design-and-build">Stunning design and build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Fagx2Fy9aPicUP3AyQ5pHk" name="Vanguard_Air_99_Wireless_keyboard_003.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Fagx2Fy9aPicUP3AyQ5pHk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 denying that the Corsair Vanguard Air 99 Wireless is a beautiful keyboard. It’s a sight for sore eyes. Fitted with a robust, well-finished aluminum frame and a plastic chassis, the Air 99 Wireless feels like it would stand the test of time. Twisting the body did not reveal any flex, and I didn’t notice the keys dipping too far into the PCB either — something I’ve experienced with all-plastic keyboards before.</p><p>Adding to the Air 99 Wireless’ durability are the double-shot PBT keycaps. These are more resistant to wear and tear than their ABS counterparts. The matte finish on the PBTs feels soft to touch, and I didn’t notice any remnants of oil or fingerprints after a full day’s work. The keycaps are lit up by customizable, per-key RGB lighting, adding a lovely gamer-esque flare to the otherwise inconspicuous board.</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="Sep9RJnS9ZNV6QN4GiNAJj" name="Vanguard_Air_99_Wireless_keyboard_010.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Sep9RJnS9ZNV6QN4GiNAJj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 after a compact, nearly full-size keyboard, the Air 99 Wireless is a superb choice. Though the keyboard uses a fair bit of aluminum in its construction, the Air 99 Wireless is still fairly lightweight at 1.98lbs. It’s even lighter than its main competition, the <a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a> ($249) which weighs 2.43lbs. I’ve had no trouble carrying it around in my already-full backpack. The Air 99 Wireless measures 16.76 x 5.42 x 0.7 inches so it doesn’t take up <em>too </em>much space on your desk either.</p><h2 id="pc-and-consoles-support">PC and consoles support</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EZBukKYYL5qcfUSSNkB2Zj" name="Vanguard_Air_99_Wireless_keyboard_017.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/EZBukKYYL5qcfUSSNkB2Zj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Vanguard Air 99 Wireless is quite a versatile keyboard, and it can be used either wired with the included USB-C cable, or wireless over Bluetooth and 2.4GHz. The keyboard comes with macOS and Windows keycaps which can be easily swapped, and there’s a toggle switch at the back that lets you change between keyboard layouts.</p><p>What’s more, the Air 99 Wireless is compatible with the <a href="https://www.tomsguide.com/reviews/ps5">PlayStation 5</a> as well as the <a href="https://www.tomsguide.com/reviews/xbox-series-x">Xbox Series X</a> and <a href="https://www.tomsguide.com/reviews/xbox-series-s">S</a> consoles. You’ll need to use the USB-A to USB-C cable to connect the keyboard to your Xbox, and you can use the 2.4GHz dongle with your PS5. There’s also a small screen on the Air 99 Wireless (more on that shortly) which you can use to put the keyboard into PS5 mode — this essentially ensures full compatibility by remapping the interface.</p><p>Personally, I prefer one of the <a href="https://www.tomsguide.com/best-picks/best-pc-game-controllers">best controllers</a> for console gaming on my PS5, but it’s good to know that this compatibility exists on the Air 99 Wireless for those who like using a keyboard and mouse. It worked well when I tried it briefly with my <a href="https://www.tomsguide.com/gaming/playstation/ps5-slim">PS5 Slim</a>, but I’d recommend doing a quick Google search to see which PS5 games are compatible with keyboards.</p><h2 id="excellent-gaming-performance-2">Excellent gaming 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="i2peh6WTon9TmRQkE3gZYj" name="Vanguard_Air_99_Wireless_keyboard_007.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/i2peh6WTon9TmRQkE3gZYj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Vanguard Air 99 Wireless is a gaming-first keyboard, and it packs performance in spades. It’s fitted with pre-lubed Corsair OPX low-profile switches which have an actuation point of 1.5mm and a total travel distance of 2.5mm. Because the travel distance is quite short, you don’t need to press the switches all the way down for your commands to be registered, which results in a less fatiguing gaming (and typing) experience.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>These switches require just 45g of force to activate too — the same as the Cherry MX Low Profile 2.0 Reds in the Cherry XTRFY MX 10.1 Wireless, and lighter than the 57g Bananas in the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). I’ve found the switches to be ultra responsive in my testing, and they won’t give you much to complain about.</p><p>The Air 99 Wireless also features N-key rollover (NKRO) with 100% anti-ghosting. NKRO, essentially, enables multiple inputs to be registered at the same time, so you can execute two commands simultaneously. As a simple example, if you press Q, A, S and D together in a document, both Q, A, S and D will appear on the screen at once.</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="CUXQauSinYF2CrJjnGpeUj" name="Vanguard_Air_99_Wireless_keyboard_018.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CUXQauSinYF2CrJjnGpeUj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, combined with the responsive switches, gave me a competitive advantage in games like Counter-Strike 2, where I was able to duck and draw my weapon at the same time. Similarly, in <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a>, I could brake or accelerate while riding my bike and swapping weapons instantaneously. I didn’t have to let go of one or the other to execute another action.</p><p>You can also enable FlashTap through the companion software, and it’s an SOCD resolution like Razer’s Snap Tap. It prioritizes the last key pressed so, for instance, you can draw to a complete stop from a sideways strafe by pressing the opposite direction key (A and D or vice versa). It’s useful in single player games, like Cyberpunk 2077, but in Counter-Strike 2 and Valve’s official servers, you’ll get kicked out, so beware.</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="gmsyrq5E4pXZKcYex2rt9f" name="Vanguard_Air_99_Wireless_keyboard_016.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gmsyrq5E4pXZKcYex2rt9f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 this, you also get 8,000Hz polling over wired and wireless connections. 8K polling is fine, but it’s mostly a gimmick that every keyboard manufacturer loves adding to their boards. We’ve seen it with the recently updated Keychron Ultra series, and now in the Air 99 Wireless. For 99% of gamers, 1,000Hz polling is good enough, and most players can’t tell the difference between 1KHz and 8KHz.</p><p>Both negate input lag, and both ensure your inputs are registered instantaneously. Using 8KHz is a massive drain on system resources and battery life — and the latter is already a troubled area for the Air 99 Wireless (more on that soon). That said, I didn’t notice any input lag or unresponsiveness over both 8,000Hz and 1,000Hz polling.</p><h2 id="lovely-switches-for-typing-too">Lovely switches for typing, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Jv9ZFeniemwLdevid5dy5f" name="Vanguard_Air_99_Wireless_keyboard_014.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Jv9ZFeniemwLdevid5dy5f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Alongside being a gaming powerhouse, the Corsair Vanguard Air 99 Wireless is a fantastic keyboard for productivity too. Its 99% layout means you’re getting a full row of Function keys as well as a number pad. The low-profile design means the keyboard has a shallow depth, so you don’t need to raise your wrists too much to reach the keys. Your hands also sit closer to the surface of the desk, which eliminates the need for a dedicated wrist rest.</p><p>Having used this keyboard for gaming as well as a full day’s worth of typing, I can confirm that it is, in fact, <em>very </em>comfortable. The Air 99 Wireless feels and sounds lovely too, featuring a gasket mount which offers a cushioned feel and reduces vibrations from key presses. There are five layers of sound dampening foam too, which lend the keyboard a muted but creamy sound profile.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Corsair Vanguard Air 99 Wireless</p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Turtle Beach Vulcan II Max</a></p></td><td  ><p>93</p></td><td  ><p>91.85%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.51</p></td><td  ><p>91.17%</p></td></tr></tbody></table></div><p>I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> to see how the Air 99 Wireless fared against other mechanical keyboards I’ve tested recently. As you can see from the table above, I was able to achieve 102WPM with 94% accuracy, and I’m very pleased. It speaks to the Air 99 Wireless’ calibre really well.</p><h2 id="intuitive-web-based-software">Intuitive web-based software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/BLyfWqdjzKAXjty6iRjrT9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/WexSPsoqQw2KVaGbiA6dQ9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/thUwxgvXpyb3AooZEvt7Q9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/4scUYc8zaZi4tBtDAVjQN9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure></figure><p>I love it when peripherals, including keyboards and mice, utilize web-based software for customization. It saves me from downloading bloatware and third-party software. The Corsair Vanguard Air 99 Wireless is accompanied by the web-based <a href="https://www.corsair.com/web-hub/index.html" target="_blank" rel="nofollow">Corsair Web Hub</a>, and to personalize your keyboard, all you need to do is plug it into your machine and you’re good to go.</p><p>Corsair Web Hub features a clean and user-friendly interface, and there are no intrusive pop-ups or obnoxious ads to ruin the user journey — unlike <em>some </em>proprietary software (looking at you, Asus Armoury Crate). With Corsair Web Hub, you can remap the keys, record macros, enable features like FlashTap, update or flash the keyboard’s firmware, and extensively adjust the RGB lighting. You can also upload GIFs (under 2MB) to display on the screen, which I found very easy to do, and within seconds, Kathryn Hahn from <a href="https://www.tomsguide.com/entertainment/disney-plus/this-is-the-one-disney-plus-show-im-watching-this-pride-month-and-its-a-binge-worthy-witchy-adventure">Agatha All Along</a> was on the Air 99 Wireless.</p><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-the-downs"><span>Corsair Vanguard Air 99 Wireless review: The downs</span></h2><p>Unfortunately, the Corsair Vanguard Air 99 Wireless has considerable flaws, such as the low-key useless screen, underwhelming battery life, finicky Stream Deck support… and <em>that </em>price tag.</p><h2 id="screen-could-be-more-useful">Screen could be more useful</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uQPVaTxSVNMJiww34t7xoe" name="Vanguard_Air_99_Wireless_keyboard_011.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/uQPVaTxSVNMJiww34t7xoe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Small TFT, LCD and even OLED screens have become a commonplace occurrence on keyboards. I’ve seen them on the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115), the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129) and the <a href="https://www.tomsguide.com/computing/peripherals/razer-blackwidow-v4-pro-75-percent-review">Razer BlackWidow V4 Pro 75%</a> ($299), to name a few. It’s then not surprising that Corsair has added a 1.9-inch LCD IPS display to the Vanguard Air 99 Wireless too. Screens on keyboards can be quite useful as they enable you to change certain settings on the fly — which is why it pains me to say that the Air 99 Wireless’ is so user-unfriendly.</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="vQftUwdbChM9RpyWoaF6kj" name="Vanguard_Air_99_Wireless_keyboard_008.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/vQftUwdbChM9RpyWoaF6kj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 screen itself is bright and responsive, but I’d be lying if I said it was easy to use. Instead of using the metallic rotary knob to swap between, say, RGB brightness and volume, you have to use a number of key combinations to access different settings. For example, to adjust the backlighting, you need to hold Fn and Num together and then rotate the dial. Why make it so complicated, Corsair?</p><p>To give you some context, adjusting settings is much easier to do on a keyboard like the Hexgears Immersion A3 TKL (the one I use when I work from home). All you need to do is push the dial up and down to access different settings. Simple, as it should be. I’ve found the screen only to be useful for seeing how much battery is left, and displaying a GIF, which I spoke about earlier. Other than that, the screen’s fairly pointless, and it’s a massive battery sink.</p><h2 id="poor-battery-life-2">Poor battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ahTsnnCRNUZZuuqRRKTJGf" name="Vanguard_Air_99_Wireless_keyboard_013.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/ahTsnnCRNUZZuuqRRKTJGf-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 brings me to my next point: the Corsair Vanguard Air 99 Wireless’ poor battery life. The keyboard is fitted with a rechargeable Lithium-ion polymer battery, and Corsair says that with the LCD screen set to 20% brightness and the RGB lighting off (over Bluetooth), the keyboard can last up to 55 hours. Crank the RGB brightness up and set the LCD brightness to 20% (over 2.4GHz), and you’re looking at a runtime of 29 hours.</p><p>In my testing, I set the RGB and LCD brightness to 100% and they chewed up the battery life, and I was able to get just about 20 hours. I won’t sugarcoat it: this is bad. Given how much you’re paying for the Air 99 Wireless, you’d expect more. The Cherry XTRFY MX 10.1 Wireless can last up to 900 hours in total, while the cheaper Keychron V1 Ultra 8K can last up to 660 hours. I’m not angry; I’m just disappointed.</p><h2 id="finicky-stream-deck-compatibility">Finicky Stream Deck compatibility</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VJsHC2WwZSxVMkvdz4syQj" name="Vanguard_Air_99_Wireless_keyboard_006.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/VJsHC2WwZSxVMkvdz4syQj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 look at the left-hand side of the Corsair Vanguard Air 99 Wireless, you’ll find a row of six programmable SD keys powered by the Elgato Stream Deck app. The keyboard packs built-in Stream Deck support, and if you download the software, you can assign specific functions and hotkeys to said row.</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="mfXVAwx5TbfQg2TkfhPmdX" name="Corsair-Air-99-app-5" alt="Stream Deck app" src="https://cdn.mos.cms.futurecdn.net/mfXVAwx5TbfQg2TkfhPmdX-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/mfXVAwx5TbfQg2TkfhPmdX-1920-80.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: Corsair / Elgato / Tom's Guide)</span></figcaption></figure><p>In theory, this sounds useful as you can launch whichever app you like with the tap of a button. In practice, I didn’t find it so straightforward. I was able to program the keys at first, but when I plugged the keyboard in and booted up the software, the Stream Deck had forgotten I’d done this before. This was rather disappointing because it could have really streamlined my workflow, but instead, I was left feeling short-changed and frustrated.</p><h2 id="overpriced">Overpriced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YsWrJwFrHR894MbwHEcQWj" name="Vanguard_Air_99_Wireless_keyboard_004.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/YsWrJwFrHR894MbwHEcQWj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the Corsair Vanguard Air 99 Wireless is expensive, and it’s frankly overpriced, given that you’re getting suboptimal battery life, a not-very-useful screen that eats up said poor battery life, and finicky Stream Deck compatibility. The Air 99 Wireless retails for a whopping <a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/" target="_blank" rel="nofollow">£239 at Amazon</a>.</p><p>Yes, it costs either the same or more than more advanced Hall Effect decks today, like the<a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-he-review"> Lemokey P1 HE</a> ($169) or the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review">Keychron Q6 HE 8K</a> ($239). Of course, you might be after a mechanical deck with lovely mech switches, but even so, I don’t think you need to spend more than $200 on a competent one. The <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-lite84-review">Lofree Flow Lite100</a> ($139) and the <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node100</a> ($109) are fantastic, cheap alternatives, and while you’ll be missing out on 8,000Hz polling, 1,000Hz is more than plenty for most games.</p><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-verdict"><span>Corsair Vanguard Air 99 Wireless 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="wa5on3V8sR6LLo8j6VtTBk" name="Vanguard_Air_99_Wireless_keyboard_005.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/wa5on3V8sR6LLo8j6VtTBk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Vanguard Air 99 Wireless is a masterpiece of design and performance, unfortunately marred by frustrating functional oversights. Typing and gaming performance are outstanding thanks to the response switches, 8,000Hz polling, and features like FlashTap. Intuitive, bloatware-free web-based software makes customizing the keyboard a walk in the park, too.</p><p>But the experience stumbles when you look past the aesthetics. While the integrated screen is bright and fun to display a GIF on, it doesn’t have much utility, and adjusting settings via key combos is frustrating. It also significantly impacts the already-struggling battery life, and the touted Stream Deck integration proved unreliable in my testing.</p><p>It’s very difficult to look past these glaring issues when the keyboard costs a whopping $259. If you prioritize raw performance and desk aesthetics above all else, the Air 99 Wireless isn’t a bad choice. But those after a polished, hassle-free daily driver will want to explore other options.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/corsair-vanguard-air-99-wireless-review</link>
                                                                            <description>
                            <![CDATA[ The Corsair Vanguard Air 99 Wireless is a powerful low-profile mechanical keyboard for gaming and typing, but its drawbacks are too problematic to overlook. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">2CCNrWy36zZ5PWQb9Gmjih</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/bsVo5GVU3wkpqoy5Mrt73k-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 08 May 2026 08:44:08 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:34 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/bsVo5GVU3wkpqoy5Mrt73k-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/bsVo5GVU3wkpqoy5Mrt73k-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">best gaming keyboards</a> can take your performance to the next level by lending you a competitive advantage. Corsair is no stranger to making some fine gaming decks, and the Vanguard Air 99 Wireless comes close to being a formidable beast — but it falls just short. The Air 99 Wireless packs ultra responsive low-profile mechanical switches, 8,000Hz polling and N-key rollover, all of which work very well.</p><p>It’s a stunning keyboard to type on too, and it's also got the looks. The keyboard is accompanied by intuitive web-based companion software… but that’s about it. Unfortunately, the Air 99 Wireless seriously falters behind the competition due to subpar battery life, finicky Stream Deck compatibility, a lackluster screen, and a frankly outrageous price tag.</p><p>For the complete breakdown, read my full Corsair Vanguard Air 99 Wireless review.</p><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-specs"><span>Corsair Vanguard Air 99 Wireless 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/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/" target="_blank" rel="nofollow">£239</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Corsair OPX Low-Profile</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>99%, 108 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows, PS5, Xbox Series X, Xbox Series S</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>Rechargeable Lithium-ion polymer</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>16.76 x 5.42 x 0.7 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.98lbs</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-corsair-vanguard-air-99-wireless-review-cheat-sheet"><span>Corsair Vanguard Air 99 Wireless review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A low-profile mechanical keyboard with dedicated Stream Deck keys and 8,000Hz polling</li><li><strong>Who is it for?</strong> For competitive gamers and serious typists… with quite a bit of money</li><li><strong>How much does it cost? </strong>The Corsair Vanguard Air 99 Wireless is available for <a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/" target="_blank" rel="nofollow">£239</a></li><li><strong>What do we like? </strong>The stunning design and build, lovely switches for typing and gaming, versatile compatibility, and intuitive web-based software</li><li><strong>What don’t we like? </strong>The low-key useless screen, finicky Stream Deck support, poor battery life, and expensive price</li></ul><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-the-ups"><span>Corsair Vanguard Air 99 Wireless review: The ups</span></h2><p>The Corsair Vanguard Air 99 Wireless is a very good looking and well-built keyboard, and it boasts excellent gaming performance and a wonderful typing experience.</p><h2 id="stunning-design-and-build">Stunning design and build</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Fagx2Fy9aPicUP3AyQ5pHk" name="Vanguard_Air_99_Wireless_keyboard_003.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Fagx2Fy9aPicUP3AyQ5pHk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 denying that the Corsair Vanguard Air 99 Wireless is a beautiful keyboard. It’s a sight for sore eyes. Fitted with a robust, well-finished aluminum frame and a plastic chassis, the Air 99 Wireless feels like it would stand the test of time. Twisting the body did not reveal any flex, and I didn’t notice the keys dipping too far into the PCB either — something I’ve experienced with all-plastic keyboards before.</p><p>Adding to the Air 99 Wireless’ durability are the double-shot PBT keycaps. These are more resistant to wear and tear than their ABS counterparts. The matte finish on the PBTs feels soft to touch, and I didn’t notice any remnants of oil or fingerprints after a full day’s work. The keycaps are lit up by customizable, per-key RGB lighting, adding a lovely gamer-esque flare to the otherwise inconspicuous board.</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="Sep9RJnS9ZNV6QN4GiNAJj" name="Vanguard_Air_99_Wireless_keyboard_010.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Sep9RJnS9ZNV6QN4GiNAJj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 after a compact, nearly full-size keyboard, the Air 99 Wireless is a superb choice. Though the keyboard uses a fair bit of aluminum in its construction, the Air 99 Wireless is still fairly lightweight at 1.98lbs. It’s even lighter than its main competition, the <a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a> ($249) which weighs 2.43lbs. I’ve had no trouble carrying it around in my already-full backpack. The Air 99 Wireless measures 16.76 x 5.42 x 0.7 inches so it doesn’t take up <em>too </em>much space on your desk either.</p><h2 id="pc-and-consoles-support">PC and consoles support</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="EZBukKYYL5qcfUSSNkB2Zj" name="Vanguard_Air_99_Wireless_keyboard_017.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/EZBukKYYL5qcfUSSNkB2Zj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Vanguard Air 99 Wireless is quite a versatile keyboard, and it can be used either wired with the included USB-C cable, or wireless over Bluetooth and 2.4GHz. The keyboard comes with macOS and Windows keycaps which can be easily swapped, and there’s a toggle switch at the back that lets you change between keyboard layouts.</p><p>What’s more, the Air 99 Wireless is compatible with the <a href="https://www.tomsguide.com/reviews/ps5">PlayStation 5</a> as well as the <a href="https://www.tomsguide.com/reviews/xbox-series-x">Xbox Series X</a> and <a href="https://www.tomsguide.com/reviews/xbox-series-s">S</a> consoles. You’ll need to use the USB-A to USB-C cable to connect the keyboard to your Xbox, and you can use the 2.4GHz dongle with your PS5. There’s also a small screen on the Air 99 Wireless (more on that shortly) which you can use to put the keyboard into PS5 mode — this essentially ensures full compatibility by remapping the interface.</p><p>Personally, I prefer one of the <a href="https://www.tomsguide.com/best-picks/best-pc-game-controllers">best controllers</a> for console gaming on my PS5, but it’s good to know that this compatibility exists on the Air 99 Wireless for those who like using a keyboard and mouse. It worked well when I tried it briefly with my <a href="https://www.tomsguide.com/gaming/playstation/ps5-slim">PS5 Slim</a>, but I’d recommend doing a quick Google search to see which PS5 games are compatible with keyboards.</p><h2 id="excellent-gaming-performance-2">Excellent gaming 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="i2peh6WTon9TmRQkE3gZYj" name="Vanguard_Air_99_Wireless_keyboard_007.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/i2peh6WTon9TmRQkE3gZYj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Vanguard Air 99 Wireless is a gaming-first keyboard, and it packs performance in spades. It’s fitted with pre-lubed Corsair OPX low-profile switches which have an actuation point of 1.5mm and a total travel distance of 2.5mm. Because the travel distance is quite short, you don’t need to press the switches all the way down for your commands to be registered, which results in a less fatiguing gaming (and typing) experience.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>These switches require just 45g of force to activate too — the same as the Cherry MX Low Profile 2.0 Reds in the Cherry XTRFY MX 10.1 Wireless, and lighter than the 57g Bananas in the <a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a> ($114). I’ve found the switches to be ultra responsive in my testing, and they won’t give you much to complain about.</p><p>The Air 99 Wireless also features N-key rollover (NKRO) with 100% anti-ghosting. NKRO, essentially, enables multiple inputs to be registered at the same time, so you can execute two commands simultaneously. As a simple example, if you press Q, A, S and D together in a document, both Q, A, S and D will appear on the screen at once.</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="CUXQauSinYF2CrJjnGpeUj" name="Vanguard_Air_99_Wireless_keyboard_018.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/CUXQauSinYF2CrJjnGpeUj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, combined with the responsive switches, gave me a competitive advantage in games like Counter-Strike 2, where I was able to duck and draw my weapon at the same time. Similarly, in <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a>, I could brake or accelerate while riding my bike and swapping weapons instantaneously. I didn’t have to let go of one or the other to execute another action.</p><p>You can also enable FlashTap through the companion software, and it’s an SOCD resolution like Razer’s Snap Tap. It prioritizes the last key pressed so, for instance, you can draw to a complete stop from a sideways strafe by pressing the opposite direction key (A and D or vice versa). It’s useful in single player games, like Cyberpunk 2077, but in Counter-Strike 2 and Valve’s official servers, you’ll get kicked out, so beware.</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="gmsyrq5E4pXZKcYex2rt9f" name="Vanguard_Air_99_Wireless_keyboard_016.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/gmsyrq5E4pXZKcYex2rt9f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 this, you also get 8,000Hz polling over wired and wireless connections. 8K polling is fine, but it’s mostly a gimmick that every keyboard manufacturer loves adding to their boards. We’ve seen it with the recently updated Keychron Ultra series, and now in the Air 99 Wireless. For 99% of gamers, 1,000Hz polling is good enough, and most players can’t tell the difference between 1KHz and 8KHz.</p><p>Both negate input lag, and both ensure your inputs are registered instantaneously. Using 8KHz is a massive drain on system resources and battery life — and the latter is already a troubled area for the Air 99 Wireless (more on that soon). That said, I didn’t notice any input lag or unresponsiveness over both 8,000Hz and 1,000Hz polling.</p><h2 id="lovely-switches-for-typing-too">Lovely switches for typing, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="Jv9ZFeniemwLdevid5dy5f" name="Vanguard_Air_99_Wireless_keyboard_014.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/Jv9ZFeniemwLdevid5dy5f-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Alongside being a gaming powerhouse, the Corsair Vanguard Air 99 Wireless is a fantastic keyboard for productivity too. Its 99% layout means you’re getting a full row of Function keys as well as a number pad. The low-profile design means the keyboard has a shallow depth, so you don’t need to raise your wrists too much to reach the keys. Your hands also sit closer to the surface of the desk, which eliminates the need for a dedicated wrist rest.</p><p>Having used this keyboard for gaming as well as a full day’s worth of typing, I can confirm that it is, in fact, <em>very </em>comfortable. The Air 99 Wireless feels and sounds lovely too, featuring a gasket mount which offers a cushioned feel and reduces vibrations from key presses. There are five layers of sound dampening foam too, which lend the keyboard a muted but creamy sound profile.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Corsair Vanguard Air 99 Wireless</p></td><td  ><p>102</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Turtle Beach Vulcan II Max</a></p></td><td  ><p>93</p></td><td  ><p>91.85%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.51</p></td><td  ><p>91.17%</p></td></tr></tbody></table></div><p>I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> to see how the Air 99 Wireless fared against other mechanical keyboards I’ve tested recently. As you can see from the table above, I was able to achieve 102WPM with 94% accuracy, and I’m very pleased. It speaks to the Air 99 Wireless’ calibre really well.</p><h2 id="intuitive-web-based-software">Intuitive web-based software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/BLyfWqdjzKAXjty6iRjrT9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/WexSPsoqQw2KVaGbiA6dQ9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/thUwxgvXpyb3AooZEvt7Q9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/4scUYc8zaZi4tBtDAVjQN9-1920-80.jpg" alt="Corsair Web Hub screenshots" /><figcaption><small role="credit">Corsair / Tom's Guide</small></figcaption></figure></figure><p>I love it when peripherals, including keyboards and mice, utilize web-based software for customization. It saves me from downloading bloatware and third-party software. The Corsair Vanguard Air 99 Wireless is accompanied by the web-based <a href="https://www.corsair.com/web-hub/index.html" target="_blank" rel="nofollow">Corsair Web Hub</a>, and to personalize your keyboard, all you need to do is plug it into your machine and you’re good to go.</p><p>Corsair Web Hub features a clean and user-friendly interface, and there are no intrusive pop-ups or obnoxious ads to ruin the user journey — unlike <em>some </em>proprietary software (looking at you, Asus Armoury Crate). With Corsair Web Hub, you can remap the keys, record macros, enable features like FlashTap, update or flash the keyboard’s firmware, and extensively adjust the RGB lighting. You can also upload GIFs (under 2MB) to display on the screen, which I found very easy to do, and within seconds, Kathryn Hahn from <a href="https://www.tomsguide.com/entertainment/disney-plus/this-is-the-one-disney-plus-show-im-watching-this-pride-month-and-its-a-binge-worthy-witchy-adventure">Agatha All Along</a> was on the Air 99 Wireless.</p><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-the-downs"><span>Corsair Vanguard Air 99 Wireless review: The downs</span></h2><p>Unfortunately, the Corsair Vanguard Air 99 Wireless has considerable flaws, such as the low-key useless screen, underwhelming battery life, finicky Stream Deck support… and <em>that </em>price tag.</p><h2 id="screen-could-be-more-useful">Screen could be more useful</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="uQPVaTxSVNMJiww34t7xoe" name="Vanguard_Air_99_Wireless_keyboard_011.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/uQPVaTxSVNMJiww34t7xoe-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Small TFT, LCD and even OLED screens have become a commonplace occurrence on keyboards. I’ve seen them on the <a href="https://www.tomsguide.com/computing/peripherals/epomaker-rt100-review">Epomaker RT100</a> ($115), the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129) and the <a href="https://www.tomsguide.com/computing/peripherals/razer-blackwidow-v4-pro-75-percent-review">Razer BlackWidow V4 Pro 75%</a> ($299), to name a few. It’s then not surprising that Corsair has added a 1.9-inch LCD IPS display to the Vanguard Air 99 Wireless too. Screens on keyboards can be quite useful as they enable you to change certain settings on the fly — which is why it pains me to say that the Air 99 Wireless’ is so user-unfriendly.</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="vQftUwdbChM9RpyWoaF6kj" name="Vanguard_Air_99_Wireless_keyboard_008.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/vQftUwdbChM9RpyWoaF6kj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 screen itself is bright and responsive, but I’d be lying if I said it was easy to use. Instead of using the metallic rotary knob to swap between, say, RGB brightness and volume, you have to use a number of key combinations to access different settings. For example, to adjust the backlighting, you need to hold Fn and Num together and then rotate the dial. Why make it so complicated, Corsair?</p><p>To give you some context, adjusting settings is much easier to do on a keyboard like the Hexgears Immersion A3 TKL (the one I use when I work from home). All you need to do is push the dial up and down to access different settings. Simple, as it should be. I’ve found the screen only to be useful for seeing how much battery is left, and displaying a GIF, which I spoke about earlier. Other than that, the screen’s fairly pointless, and it’s a massive battery sink.</p><h2 id="poor-battery-life-2">Poor battery life</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ahTsnnCRNUZZuuqRRKTJGf" name="Vanguard_Air_99_Wireless_keyboard_013.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/ahTsnnCRNUZZuuqRRKTJGf-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 brings me to my next point: the Corsair Vanguard Air 99 Wireless’ poor battery life. The keyboard is fitted with a rechargeable Lithium-ion polymer battery, and Corsair says that with the LCD screen set to 20% brightness and the RGB lighting off (over Bluetooth), the keyboard can last up to 55 hours. Crank the RGB brightness up and set the LCD brightness to 20% (over 2.4GHz), and you’re looking at a runtime of 29 hours.</p><p>In my testing, I set the RGB and LCD brightness to 100% and they chewed up the battery life, and I was able to get just about 20 hours. I won’t sugarcoat it: this is bad. Given how much you’re paying for the Air 99 Wireless, you’d expect more. The Cherry XTRFY MX 10.1 Wireless can last up to 900 hours in total, while the cheaper Keychron V1 Ultra 8K can last up to 660 hours. I’m not angry; I’m just disappointed.</p><h2 id="finicky-stream-deck-compatibility">Finicky Stream Deck compatibility</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VJsHC2WwZSxVMkvdz4syQj" name="Vanguard_Air_99_Wireless_keyboard_006.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/VJsHC2WwZSxVMkvdz4syQj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 look at the left-hand side of the Corsair Vanguard Air 99 Wireless, you’ll find a row of six programmable SD keys powered by the Elgato Stream Deck app. The keyboard packs built-in Stream Deck support, and if you download the software, you can assign specific functions and hotkeys to said row.</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="mfXVAwx5TbfQg2TkfhPmdX" name="Corsair-Air-99-app-5" alt="Stream Deck app" src="https://cdn.mos.cms.futurecdn.net/mfXVAwx5TbfQg2TkfhPmdX-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1510" height="850" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/mfXVAwx5TbfQg2TkfhPmdX-1920-80.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: Corsair / Elgato / Tom's Guide)</span></figcaption></figure><p>In theory, this sounds useful as you can launch whichever app you like with the tap of a button. In practice, I didn’t find it so straightforward. I was able to program the keys at first, but when I plugged the keyboard in and booted up the software, the Stream Deck had forgotten I’d done this before. This was rather disappointing because it could have really streamlined my workflow, but instead, I was left feeling short-changed and frustrated.</p><h2 id="overpriced">Overpriced</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="YsWrJwFrHR894MbwHEcQWj" name="Vanguard_Air_99_Wireless_keyboard_004.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/YsWrJwFrHR894MbwHEcQWj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the Corsair Vanguard Air 99 Wireless is expensive, and it’s frankly overpriced, given that you’re getting suboptimal battery life, a not-very-useful screen that eats up said poor battery life, and finicky Stream Deck compatibility. The Air 99 Wireless retails for a whopping <a href="https://www.amazon.com/Corsair-Vanguard-Wireless-Low-Profile-Optical-Mechanical/dp/B0GMR9V158/" target="_blank" rel="nofollow">$259</a> / <a href="https://www.amazon.co.uk/Corsair-VANGUARD-WIRELESS-Low-Profile-Optical-Mechanical/dp/B0GPRHFSDX/" target="_blank" rel="nofollow">£239 at Amazon</a>.</p><p>Yes, it costs either the same or more than more advanced Hall Effect decks today, like the<a href="https://www.tomsguide.com/computing/peripherals/lemokey-p1-he-review"> Lemokey P1 HE</a> ($169) or the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review">Keychron Q6 HE 8K</a> ($239). Of course, you might be after a mechanical deck with lovely mech switches, but even so, I don’t think you need to spend more than $200 on a competent one. The <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-lite84-review">Lofree Flow Lite100</a> ($139) and the <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node100</a> ($109) are fantastic, cheap alternatives, and while you’ll be missing out on 8,000Hz polling, 1,000Hz is more than plenty for most games.</p><h2 class="article-body__section" id="section-corsair-vanguard-air-99-wireless-review-verdict"><span>Corsair Vanguard Air 99 Wireless 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="wa5on3V8sR6LLo8j6VtTBk" name="Vanguard_Air_99_Wireless_keyboard_005.JPG" alt="A black Corsair Vanguard Air 99 Wireless low-profile mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/wa5on3V8sR6LLo8j6VtTBk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Corsair Vanguard Air 99 Wireless is a masterpiece of design and performance, unfortunately marred by frustrating functional oversights. Typing and gaming performance are outstanding thanks to the response switches, 8,000Hz polling, and features like FlashTap. Intuitive, bloatware-free web-based software makes customizing the keyboard a walk in the park, too.</p><p>But the experience stumbles when you look past the aesthetics. While the integrated screen is bright and fun to display a GIF on, it doesn’t have much utility, and adjusting settings via key combos is frustrating. It also significantly impacts the already-struggling battery life, and the touted Stream Deck integration proved unreliable in my testing.</p><p>It’s very difficult to look past these glaring issues when the keyboard costs a whopping $259. If you prioritize raw performance and desk aesthetics above all else, the Air 99 Wireless isn’t a bad choice. But those after a polished, hassle-free daily driver will want to explore other options.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I've been testing the IQUNIX Magi65 Pro for a few weeks, and I think my months-long search for the right keyboard is over ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Since I switched to mechanical keyboards, my desk is often occupied by a pile of them. You see, I’ve struggled to stick to a mechanical keyboard for more than a few months. </p><p>On one hand, traditional, full-size models are loud and slow me down, and on the other, the quiet alternatives feel too soft and just aren’t as satisfying to type on. Over the last few weeks, I’ve been testing IQUNIX's Magi65 Pro, and it may be here to stay. </p><p>IQUNIX is a Chinese keyboard maker that has carved out a niche for itself with a variety of standout, high-quality designs. Its Magi Pro line of keyboards is unlike anything I’ve tried before, and pairs together a unique combination of elements for a sleek balance between typing experience and functionality. </p><p>Its flat, low-profile keys are quiet, without compromising much on travel, and a panel on the right houses a dedicated volume roller, X-shaped media controls, a magnetic slot for the 2.4GHz dongle, and, surprisingly, a pulsating accent light bar. </p><p>At $139, the IQUNIX Magi65 Pro could become the new, default option, especially for first-time mechanical keyboard buyers. </p><h2 class="article-body__section" id="section-design-and-build"><span>Design and Build</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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VEXmdXoovwRXLpHSXeYfDN" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/VEXmdXoovwRXLpHSXeYfDN-1920-80.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: Iquinix)</span></figcaption></figure><p>As soon as you unbox the Magi65 Pro, you’re reminded of the onus IQUNIX has on premium materials. Its accessory kit includes additional keycaps for macOS/Windows and a handful of decorative ones, two of which are metallic. You’ll also find in there an aluminium keycap and switch puller and a braided USB-C-to-A cable. </p><p>IQUNIX's Magi65 Pro is a 65% low-profile mechanical keyboard, but you can opt for larger sizes, like 75% and 95%. There’s a cheaper, non-Pro version, as well, that omits the media pad altogether for an even more compact form factor. </p><p>The Magi65 Pro’s chassis is made out of an aluminium alloy, which not only looks nice and clean, but also has the structural heft that anchors it to surfaces and enables a sturdy and stable base for you to type. </p><p>Though it sits on a slightly elevated tray, the Magi65 has a flat typing angle because of the slim 11mm base height. Plus, with no adjustable feet, the design doesn’t, at first, feel conducive to a smooth typing 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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="m86sMQc64xbGL38oCBceyB" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/m86sMQc64xbGL38oCBceyB-1920-80.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>However, I got used to it quickly, thanks in part to the smooth Gold Red switches and the equally flat keycaps. Speaking of which, I liked that the keys have an anti-oil coating, which keeps them clean and resistant to wear and grease, even after prolonged use.</p><p>That said, while the raised base worked well for my office desk, a pair of adjustable feet would have made the Magi65 far more versatile in varying conditions. Also missing is a separate on/off toggle, and you instead have to rely on a combination of keys, a design choice particularly annoying when you are new to it. </p><p>What sets apart the Magi65 Pro’s design from other keyboards is its media panel. I was surprised to find how thoughtfully each component is made to fit its purpose. </p><p>The scroll wheel for volume, for example, has a ridged and grippy texture for more precise control, the four-way pad features firm and tactile buttons, and the housing for the wireless receiver has a magnetic cover you can easily lift and place back on. </p><p>Though I largely ended up using these as fidget toys, you can get more value out of them by personalizing their actions and assigning them to custom functions. </p><h2 class="article-body__section" id="section-typing-experience-sound-and-software"><span>Typing experience, sound, and software </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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7TnjqbVmXft9hekmyFZtQA" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/7TnjqbVmXft9hekmyFZtQA-1920-80.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 Magi65 Pro’s low-profile keys offer the best of two worlds: they have a satisfyingly tactile and bouncy response, and since they are not traditional full-height ones, they are not as loud, and I didn’t have to work too hard to hit my best typing speeds. </p><p>In the weeks I’ve been testing it, the Magi65 Pro has been a joy to use and will suit those who are looking for a premium mechanical keyboard, minus the typical compromises, especially in workplaces. </p><p>For software, IQUNIX has resorted to the open-source, web-based program called VIA. From here, you can tweak the Magi65 Pro as you wish, from switching up what each key does, how intense the RGB lighting is, and the media pad’s functions. </p><p>However, for first-time mechanical keyboard owners, VIA can prove frustrating. It often throws technical errors, in which case you’ll have to track down and upload a JSON file that informs VIA exactly how your specific keyboard is laid out. </p><p>Once I had that up and running, though, customizing through VIA was a breeze, and much better than learning the key combinations needed for, for example, RGB personalization. </p><h2 class="article-body__section" id="section-connectivity-and-battery-life"><span>Connectivity and 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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="qzHP4qZBnwyh3DAM52EVqB" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/qzHP4qZBnwyh3DAM52EVqB-1920-80.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 Magi65 Pro supports wired and wireless connections (both 2.4 GHz and Bluetooth 5.1), and you can have all three active at the same time and switch between them. Connectivity worked as intended, with no noticeable drops or wakeup delays. </p><p>Battery life, on the other hand, was a hit and a miss. With RGB lighting on, the keyboard’s 3000mAh pack lasts a workday and no more than 6-7 hours. Switching it off, however, pushes its endurance into triple-digits, about 250-300 hours. While it does aggressively go into standby when not in use, I still found myself charging it every other day when the lights were on. </p><h2 class="article-body__section" id="section-verdict"><span>Verdict</span></h2><p>I was apprehensive about the IQUNIX Magi65 Pro, largely due to its low typing angle, and though it wasn’t without its quirks, it didn’t take long for it to occupy a permanent spot on my desk. </p><p>It perfectly pairs the convenience of a low-profile board with the tactile satisfaction of a traditional mechanical one. Throw in the added perks, like the customizable media pad and an aluminium chassis, and the $139 IQUNIX Magi65 Pro is hard to fault. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/iquinix-magi65-pro-review</link>
                                                                            <description>
                            <![CDATA[ After months of searching, the IQUNIX Magi65 Pro has finally cleared the clutter on my desk. Here is why this $139 keyboard is a low-profile winner. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">4CZ2MNBQHNMmcuVMfjT4XX</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/mELvxsgXFGt2xPHoxH5KDC-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Mon, 04 May 2026 10:00:00 +0000</pubDate>                                                                                                                                <updated>Thu, 07 May 2026 12:34:44 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                                                                                    <dc:creator><![CDATA[ Shubham Agarwal ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/s7PpLkDRvFn2TAmUK6kHpD-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Shubham Agarwal is a freelance tech journalist based in India. His work has previously appeared on WSJ, NatGeo, BBC, Wired, and others.&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/mELvxsgXFGt2xPHoxH5KDC-1920-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Iquinix Magi65 Pro]]></media:description>                                                            <media:text><![CDATA[Iquinix Magi65 Pro]]></media:text>
                                <media:title type="plain"><![CDATA[Iquinix Magi65 Pro]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/mELvxsgXFGt2xPHoxH5KDC-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Since I switched to mechanical keyboards, my desk is often occupied by a pile of them. You see, I’ve struggled to stick to a mechanical keyboard for more than a few months. </p><p>On one hand, traditional, full-size models are loud and slow me down, and on the other, the quiet alternatives feel too soft and just aren’t as satisfying to type on. Over the last few weeks, I’ve been testing IQUNIX's Magi65 Pro, and it may be here to stay. </p><p>IQUNIX is a Chinese keyboard maker that has carved out a niche for itself with a variety of standout, high-quality designs. Its Magi Pro line of keyboards is unlike anything I’ve tried before, and pairs together a unique combination of elements for a sleek balance between typing experience and functionality. </p><p>Its flat, low-profile keys are quiet, without compromising much on travel, and a panel on the right houses a dedicated volume roller, X-shaped media controls, a magnetic slot for the 2.4GHz dongle, and, surprisingly, a pulsating accent light bar. </p><p>At $139, the IQUNIX Magi65 Pro could become the new, default option, especially for first-time mechanical keyboard buyers. </p><h2 class="article-body__section" id="section-design-and-build"><span>Design and Build</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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="VEXmdXoovwRXLpHSXeYfDN" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/VEXmdXoovwRXLpHSXeYfDN-1920-80.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: Iquinix)</span></figcaption></figure><p>As soon as you unbox the Magi65 Pro, you’re reminded of the onus IQUNIX has on premium materials. Its accessory kit includes additional keycaps for macOS/Windows and a handful of decorative ones, two of which are metallic. You’ll also find in there an aluminium keycap and switch puller and a braided USB-C-to-A cable. </p><p>IQUNIX's Magi65 Pro is a 65% low-profile mechanical keyboard, but you can opt for larger sizes, like 75% and 95%. There’s a cheaper, non-Pro version, as well, that omits the media pad altogether for an even more compact form factor. </p><p>The Magi65 Pro’s chassis is made out of an aluminium alloy, which not only looks nice and clean, but also has the structural heft that anchors it to surfaces and enables a sturdy and stable base for you to type. </p><p>Though it sits on a slightly elevated tray, the Magi65 has a flat typing angle because of the slim 11mm base height. Plus, with no adjustable feet, the design doesn’t, at first, feel conducive to a smooth typing 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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="m86sMQc64xbGL38oCBceyB" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/m86sMQc64xbGL38oCBceyB-1920-80.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>However, I got used to it quickly, thanks in part to the smooth Gold Red switches and the equally flat keycaps. Speaking of which, I liked that the keys have an anti-oil coating, which keeps them clean and resistant to wear and grease, even after prolonged use.</p><p>That said, while the raised base worked well for my office desk, a pair of adjustable feet would have made the Magi65 far more versatile in varying conditions. Also missing is a separate on/off toggle, and you instead have to rely on a combination of keys, a design choice particularly annoying when you are new to it. </p><p>What sets apart the Magi65 Pro’s design from other keyboards is its media panel. I was surprised to find how thoughtfully each component is made to fit its purpose. </p><p>The scroll wheel for volume, for example, has a ridged and grippy texture for more precise control, the four-way pad features firm and tactile buttons, and the housing for the wireless receiver has a magnetic cover you can easily lift and place back on. </p><p>Though I largely ended up using these as fidget toys, you can get more value out of them by personalizing their actions and assigning them to custom functions. </p><h2 class="article-body__section" id="section-typing-experience-sound-and-software"><span>Typing experience, sound, and software </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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="7TnjqbVmXft9hekmyFZtQA" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/7TnjqbVmXft9hekmyFZtQA-1920-80.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 Magi65 Pro’s low-profile keys offer the best of two worlds: they have a satisfyingly tactile and bouncy response, and since they are not traditional full-height ones, they are not as loud, and I didn’t have to work too hard to hit my best typing speeds. </p><p>In the weeks I’ve been testing it, the Magi65 Pro has been a joy to use and will suit those who are looking for a premium mechanical keyboard, minus the typical compromises, especially in workplaces. </p><p>For software, IQUNIX has resorted to the open-source, web-based program called VIA. From here, you can tweak the Magi65 Pro as you wish, from switching up what each key does, how intense the RGB lighting is, and the media pad’s functions. </p><p>However, for first-time mechanical keyboard owners, VIA can prove frustrating. It often throws technical errors, in which case you’ll have to track down and upload a JSON file that informs VIA exactly how your specific keyboard is laid out. </p><p>Once I had that up and running, though, customizing through VIA was a breeze, and much better than learning the key combinations needed for, for example, RGB personalization. </p><h2 class="article-body__section" id="section-connectivity-and-battery-life"><span>Connectivity and 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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="qzHP4qZBnwyh3DAM52EVqB" name="Iquinix Magi65 Pro" alt="Iquinix Magi65 Pro" src="https://cdn.mos.cms.futurecdn.net/qzHP4qZBnwyh3DAM52EVqB-1920-80.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 Magi65 Pro supports wired and wireless connections (both 2.4 GHz and Bluetooth 5.1), and you can have all three active at the same time and switch between them. Connectivity worked as intended, with no noticeable drops or wakeup delays. </p><p>Battery life, on the other hand, was a hit and a miss. With RGB lighting on, the keyboard’s 3000mAh pack lasts a workday and no more than 6-7 hours. Switching it off, however, pushes its endurance into triple-digits, about 250-300 hours. While it does aggressively go into standby when not in use, I still found myself charging it every other day when the lights were on. </p><h2 class="article-body__section" id="section-verdict"><span>Verdict</span></h2><p>I was apprehensive about the IQUNIX Magi65 Pro, largely due to its low typing angle, and though it wasn’t without its quirks, it didn’t take long for it to occupy a permanent spot on my desk. </p><p>It perfectly pairs the convenience of a low-profile board with the tactile satisfaction of a traditional mechanical one. Throw in the added perks, like the customizable media pad and an aluminium chassis, and the $139 IQUNIX Magi65 Pro is hard to fault. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just spent 48 hours with the Steam Controller — here's my honest verdict ]]></title>
                                                                                                <dc:content><![CDATA[ <p>The <a href="https://www.tomsguide.com/gaming/gaming-peripherals/im-a-lifelong-gamer-and-i-cant-wait-to-try-the-steam-controller-3-reasons-why">Steam Controller</a> is the real deal. This is my conclusion after having used Valve’s new controller for over 48 hours. Not only is it the perfect controller for Steam users, but it’s also a smart redesign of the controversial original from 11 years ago. It’s a brilliant peripheral that only makes me yearn for the Steam Machine even more.</p><p>On top of a more traditional controller layout than its predecessor, the Steam Controller has unique features, such as large haptic touchpads, four back buttons, TMR thumbsticks, and a slew of software-specific customization options. The latter is particularly noteworthy, as you can fine-tune the controller for virtually any game genre. It also just feels good to hold thanks to its ergonomic, lightweight design.</p><p>I’ll have a full review of the Steam Controller for you soon. For now, here are my impressions after 48 hours with the peripheral.</p><div class="product"><a data-dimension112="c07e72b5-5e54-4e69-b1e6-3301cf179d25" data-action="Deal Block" data-label="Steam Controller" data-dimension48="Steam Controller" data-dimension25="$99" href="https://store.steampowered.com/sale/steamcontroller" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1232px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="p4kRCykqzQRFswEntyNcUE" name="Steam Controller" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/p4kRCykqzQRFswEntyNcUE-1920-80.jpg" mos="" align="middle" fullscreen="" width="1232" height="1232" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The new Steam Controller could be one of the best PC game controllers thanks to its Magnetic thumbsticks, a full set of inputs for your Steam games, haptic motors, and claimed 35+ hours of battery life. It's not on sale yet, but the product page says it will be available on May 4.<br><a class="view-deal button" href="https://store.steampowered.com/sale/steamcontroller" target="_blank" rel="nofollow" data-dimension112="c07e72b5-5e54-4e69-b1e6-3301cf179d25" data-action="Deal Block" data-label="Steam Controller" data-dimension48="Steam Controller" data-dimension25="$99">View Deal</a></p></div><h2 id="first-impressions">First impressions</h2><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="VA2D2h27873kpRELmPwZHS" name="Steam Controller-1-LIST" alt="The Steam Controller on a desk." src="https://cdn.mos.cms.futurecdn.net/VA2D2h27873kpRELmPwZHS-1920-80.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 official press shots and even reviewers’ photos made the controller appear as big as an old Dreamcast controller, but in reality, it’s just slightly wider and longer than a modern Xbox controller. I do have large hands, so folks with smaller hands might feel differently, but this controller fits my grip nicely.</p><p>The controller has the same matte-like finish as the Steam Deck. Thanks to that, it's easy to maintain your grip, even if your hands get sweaty. The ABXY face buttons and shoulder buttons feel like a more rigid, durable version of their Steam Deck equivalents. In fact, you could almost consider this controller a Steam Deck without a screen since it feels so similar.</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="2on6Q6pXBJRLv9qybxnqm9" name="Steam Controller-8" alt="Steam Controller puck" src="https://cdn.mos.cms.futurecdn.net/2on6Q6pXBJRLv9qybxnqm9-1920-80.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 Steam Controller is extremely simple. All you have to do is connect the included receiver puck to your PC via the USB cable, then magnetically snap it to the controller to begin pairing. Snapping the puck on and pulling it off is immensely fun and reminds me of doing the same with MagSafe on MacBooks.</p><h2 id="the-gaming-experience">The gaming experience</h2><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="NcVbKsrfkY4rawQRAzDuYV" name="Steam Controller-11" alt="Steam Controller" src="https://cdn.mos.cms.futurecdn.net/NcVbKsrfkY4rawQRAzDuYV-1920-80.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>You know a controller is great when it becomes a natural extension of your hands. That has largely been my experience with the Steam Controller, since it’s so intuitive to use. The TMR thumbsticks respond to even the smallest movements, while the buttons have a nice level of resistance when pressed. And as I said before, it feels awesome simply holding it.</p><p>When exploring the open-world setting of <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a>, I had no issues moving around and taking in the dystopian game’s sights. Whether I was walking down a neon-lit street or blasting cyborgs away with an assault rifle, everything felt just right. The triggers are excellent for shooting games and racers alike.</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="wXqarwHGksv9v9Rda4jNQG" name="Steam Controller-13" alt="Steam Controller customization" src="https://cdn.mos.cms.futurecdn.net/wXqarwHGksv9v9Rda4jNQG-1920-80.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>There are a ton of customization options within SteamOS’ Steam Input settings menu. Not only can you remap all the buttons, but you can even program the touchpads to act like a mouse or even a D-Pad. I haven’t toyed around with all the settings, but trust me, you could spend hours messing around with them. This high level of customization lets you tailor controls for specific games and genres.</p><p>I don’t play genres like RTS or MMOs that would make greater use of touchpads, but I do enjoy their haptic feedback when I scroll through menus. This isn’t a game-changing feature, but little touches like this make the experience more enjoyable and immersive.</p><p>Except for fighting games, where I prefer using a <a href="https://www.tomsguide.com/gaming/gaming-peripherals/i-swapped-my-controller-for-an-arcade-stick-and-fighting-games-will-never-be-the-same">dedicated fight stick</a>, I don’t see myself using another controller for PC gaming since it works so well for the main genres I play.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-evvmme"></div>                            </div>                            <script src="https://kwizly.com/embed/evvmme.js" async></script><h2 id="outlook-2">Outlook</h2><p>My full review will be posted soon, but suffice it to say that the Steam Controller more than lives up to the hype. It’s a brilliantly designed peripheral that feels like a natural extension of the Steam ecosystem. While it’s not compatible with other PC game launchers, Steam users will be more than happy with this product.</p><p>The Steam Controller is already great on its own, but it’s a nice appetizer for the full Steam Machine experience. If that platform can deliver the same smooth and intuitive experience, we’re in for something special. But for now, we can enjoy the Steam Controller for what it delivers—and what it delivers is pretty darn phenomenal.</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/gaming/gaming-peripherals/steam-controller-review-roundup">Steam Controller reviews are in: 'I can’t imagine playing my PC games with anything else'</a></li><li><a href="https://www.tomsguide.com/computing/monitors/lg-ultragear-27gx790b-b-review">I just tested the world’s fastest OLED gaming monitor — and its refresh rates are insane</a></li><li><a href="https://www.tomsguide.com/computing/gaming-laptops/nvidia-rtx-5070-laptop-gpu-officially-has-12gb-of-vram-and-its-about-time">Nvidia RTX 5070 laptop GPU gets 12GB VRAM — here’s why it's a game-changer</a></li></ul> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/peripherals/i-just-spent-48-hours-with-the-steam-controller-heres-my-honest-verdict</link>
                                                                            <description>
                            <![CDATA[ I tested the Steam Controller for 48 hours to see if it lives up to the hype. Here's what I think of Valve's latest peripheral. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">aQ5o8nDeK6y2tuGQHDNu3G</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/HJ6ZRxVDUjBBGPfdRCPNrm-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sat, 02 May 2026 07:19:41 +0000</pubDate>                                                                                                                                                                                                                                <category><![CDATA[Computing Peripherals]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                                                                <author><![CDATA[ tony.polanco@futurenet.com (Tony Polanco) ]]></author>                    <dc:creator><![CDATA[ Tony Polanco ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/atzRNqFt5wYgEUPBDahWsD-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ null ]]></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/HJ6ZRxVDUjBBGPfdRCPNrm-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Steam Controller]]></media:description>                                                            <media:text><![CDATA[Steam Controller]]></media:text>
                                <media:title type="plain"><![CDATA[Steam Controller]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/HJ6ZRxVDUjBBGPfdRCPNrm-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>The <a href="https://www.tomsguide.com/gaming/gaming-peripherals/im-a-lifelong-gamer-and-i-cant-wait-to-try-the-steam-controller-3-reasons-why">Steam Controller</a> is the real deal. This is my conclusion after having used Valve’s new controller for over 48 hours. Not only is it the perfect controller for Steam users, but it’s also a smart redesign of the controversial original from 11 years ago. It’s a brilliant peripheral that only makes me yearn for the Steam Machine even more.</p><p>On top of a more traditional controller layout than its predecessor, the Steam Controller has unique features, such as large haptic touchpads, four back buttons, TMR thumbsticks, and a slew of software-specific customization options. The latter is particularly noteworthy, as you can fine-tune the controller for virtually any game genre. It also just feels good to hold thanks to its ergonomic, lightweight design.</p><p>I’ll have a full review of the Steam Controller for you soon. For now, here are my impressions after 48 hours with the peripheral.</p><div class="product"><a data-dimension112="c07e72b5-5e54-4e69-b1e6-3301cf179d25" data-action="Deal Block" data-label="Steam Controller" data-dimension48="Steam Controller" data-dimension25="$99" href="https://store.steampowered.com/sale/steamcontroller" target="_blank" rel="nofollow"><figure class="van-image-figure "  ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1232px;"><p class="vanilla-image-block" style="padding-top:100.00%;"><img id="p4kRCykqzQRFswEntyNcUE" name="Steam Controller" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/p4kRCykqzQRFswEntyNcUE-1920-80.jpg" mos="" align="middle" fullscreen="" width="1232" height="1232" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The new Steam Controller could be one of the best PC game controllers thanks to its Magnetic thumbsticks, a full set of inputs for your Steam games, haptic motors, and claimed 35+ hours of battery life. It's not on sale yet, but the product page says it will be available on May 4.<br><a class="view-deal button" href="https://store.steampowered.com/sale/steamcontroller" target="_blank" rel="nofollow" data-dimension112="c07e72b5-5e54-4e69-b1e6-3301cf179d25" data-action="Deal Block" data-label="Steam Controller" data-dimension48="Steam Controller" data-dimension25="$99">View Deal</a></p></div><h2 id="first-impressions">First impressions</h2><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="VA2D2h27873kpRELmPwZHS" name="Steam Controller-1-LIST" alt="The Steam Controller on a desk." src="https://cdn.mos.cms.futurecdn.net/VA2D2h27873kpRELmPwZHS-1920-80.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 official press shots and even reviewers’ photos made the controller appear as big as an old Dreamcast controller, but in reality, it’s just slightly wider and longer than a modern Xbox controller. I do have large hands, so folks with smaller hands might feel differently, but this controller fits my grip nicely.</p><p>The controller has the same matte-like finish as the Steam Deck. Thanks to that, it's easy to maintain your grip, even if your hands get sweaty. The ABXY face buttons and shoulder buttons feel like a more rigid, durable version of their Steam Deck equivalents. In fact, you could almost consider this controller a Steam Deck without a screen since it feels so similar.</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="2on6Q6pXBJRLv9qybxnqm9" name="Steam Controller-8" alt="Steam Controller puck" src="https://cdn.mos.cms.futurecdn.net/2on6Q6pXBJRLv9qybxnqm9-1920-80.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 Steam Controller is extremely simple. All you have to do is connect the included receiver puck to your PC via the USB cable, then magnetically snap it to the controller to begin pairing. Snapping the puck on and pulling it off is immensely fun and reminds me of doing the same with MagSafe on MacBooks.</p><h2 id="the-gaming-experience">The gaming experience</h2><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="NcVbKsrfkY4rawQRAzDuYV" name="Steam Controller-11" alt="Steam Controller" src="https://cdn.mos.cms.futurecdn.net/NcVbKsrfkY4rawQRAzDuYV-1920-80.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>You know a controller is great when it becomes a natural extension of your hands. That has largely been my experience with the Steam Controller, since it’s so intuitive to use. The TMR thumbsticks respond to even the smallest movements, while the buttons have a nice level of resistance when pressed. And as I said before, it feels awesome simply holding it.</p><p>When exploring the open-world setting of <a href="https://www.tomsguide.com/reviews/cyberpunk-2077-phantom-liberty">Cyberpunk 2077</a>, I had no issues moving around and taking in the dystopian game’s sights. Whether I was walking down a neon-lit street or blasting cyborgs away with an assault rifle, everything felt just right. The triggers are excellent for shooting games and racers alike.</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="wXqarwHGksv9v9Rda4jNQG" name="Steam Controller-13" alt="Steam Controller customization" src="https://cdn.mos.cms.futurecdn.net/wXqarwHGksv9v9Rda4jNQG-1920-80.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>There are a ton of customization options within SteamOS’ Steam Input settings menu. Not only can you remap all the buttons, but you can even program the touchpads to act like a mouse or even a D-Pad. I haven’t toyed around with all the settings, but trust me, you could spend hours messing around with them. This high level of customization lets you tailor controls for specific games and genres.</p><p>I don’t play genres like RTS or MMOs that would make greater use of touchpads, but I do enjoy their haptic feedback when I scroll through menus. This isn’t a game-changing feature, but little touches like this make the experience more enjoyable and immersive.</p><p>Except for fighting games, where I prefer using a <a href="https://www.tomsguide.com/gaming/gaming-peripherals/i-swapped-my-controller-for-an-arcade-stick-and-fighting-games-will-never-be-the-same">dedicated fight stick</a>, I don’t see myself using another controller for PC gaming since it works so well for the main genres I play.</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-evvmme"></div>                            </div>                            <script src="https://kwizly.com/embed/evvmme.js" async></script><h2 id="outlook-2">Outlook</h2><p>My full review will be posted soon, but suffice it to say that the Steam Controller more than lives up to the hype. It’s a brilliantly designed peripheral that feels like a natural extension of the Steam ecosystem. While it’s not compatible with other PC game launchers, Steam users will be more than happy with this product.</p><p>The Steam Controller is already great on its own, but it’s a nice appetizer for the full Steam Machine experience. If that platform can deliver the same smooth and intuitive experience, we’re in for something special. But for now, we can enjoy the Steam Controller for what it delivers—and what it delivers is pretty darn phenomenal.</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/gaming/gaming-peripherals/steam-controller-review-roundup">Steam Controller reviews are in: 'I can’t imagine playing my PC games with anything else'</a></li><li><a href="https://www.tomsguide.com/computing/monitors/lg-ultragear-27gx790b-b-review">I just tested the world’s fastest OLED gaming monitor — and its refresh rates are insane</a></li><li><a href="https://www.tomsguide.com/computing/gaming-laptops/nvidia-rtx-5070-laptop-gpu-officially-has-12gb-of-vram-and-its-about-time">Nvidia RTX 5070 laptop GPU gets 12GB VRAM — here’s why it's a game-changer</a></li></ul>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I just tested the foldable Keychron B11 Pro Ultra-Slim keyboard — it’s good for typing but it might leave you feeling shortchanged ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Keychron is no stranger to creating some of the <a href="https://www.tomsguide.com/best-picks/best-wireless-keyboard">best wireless keyboards</a> out there. In 2026, it decided to do something different and launch its first-ever folding keyboard. I’ve been testing the Keychron B11 Pro, and for the most part, it’s a delight. This is an ultra-slim keyboard that you can take on the go, or even slip into your pocket. Its scissor switches and concave keycaps offer a lovely typing experience, and customizing the keyboard is super easy too, thanks to web-based software.</p><p>But it’s far from perfect. Though it’s great to type on and its split Alice layout is comfortable for your wrists, there aren’t many other ergonomic benefits. There’s also a steep learning curve if you’re coming from traditional keyboards, and there’s no backlighting.</p><p>For the complete breakdown, read my full Keychron B11 Pro.</p><h2 class="article-body__section" id="section-keychron-b11-pro-review-specs"><span>Keychron B11 Pro review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.keychron.com/products/keychron-b11-pro-ultra-slim-wireless-foldable-keyboard" target="_blank" rel="nofollow">$64</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Scissor</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>ABS</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>65% Alice, 66 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows, Android, iOS, iPadOS</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz) | 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>250mAh rechargeable Li-polymer</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>15.45 x 5.62 x 0.2 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>9.1oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Deep Space Gray</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-keychron-b11-pro-review-cheat-sheet"><span>Keychron B11 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>An ultra-slim and portable productivity keyboard with scissor switches</li><li><strong>Who is it for?</strong> Anyone who travels a lot for work, or those on a budget</li><li><strong>How much does it cost? </strong>The Keychron B11 Pro is available for <a href="https://www.keychron.com/products/keychron-b11-pro-ultra-slim-wireless-foldable-keyboard" target="_blank" rel="nofollow">$64</a></li><li><strong>What do we like?</strong> The portable design, solid build quality, great typing experience, web-based software, and affordable price point</li><li><strong>What don’t we like? </strong>The non-ergonomic design, steep learning curve, and lack of backlighting</li></ul><h2 class="article-body__section" id="section-keychron-b11-pro-review-the-ups"><span>Keychron B11 Pro review: The ups</span></h2><p>The Keychron B11 Pro is extremely portable, and it offers a lovely typing experience. Oh, and customization is as easy as pie through web-based software, too.</p><h2 id="so-so-slim-and-portable">So, so slim and portable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8gvvhcjKMroztgTTucYzqk" name="Keychron_B11_Pro_Slimline_keyboard_004.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/8gvvhcjKMroztgTTucYzqk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Keychron is no stranger to creating keyboards that are highly portable. Just look at the <a href="https://www.tomsguide.com/computing/peripherals/keychron-b1-pro-review">Keychron B1 Pro</a> ($39) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-b2-pro-review">B2 Pro</a> ($44). As part of the B-series, the Keychron B11 Pro is no different, but it has a neat trick up its sleeve: it can be folded to cut the size in half, making it <em>the </em>most portable keyboard I’ve tested so far.</p><p>When unfolded, the keyboard measures 15.45 x 5.62 x 0.2 inches, so you’re looking at about eight inches of space needed in your bag to stash the B11 Pro. It’s genius, really. The keyboard weighs next to nothing too, at 9.1oz. I’ve had no trouble carrying it in my backpack, which also included two boxed-up pairs of headphones, my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a>, and a few other small products. If you have big enough pockets, you’d probably be able to slip in the B11 Pro too. It’s more foldable and pocketable than its primary competition, the <a href="https://www.tomsguide.com/computing/peripherals/seenda-foldable-bluetooth-keyboard-with-touchpad-review">Seenda Foldable</a> ($46).</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="w9t4g436tW7HzEam6ea3Kk" name="Keychron_B11_Pro_Slimline_keyboard_011.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/w9t4g436tW7HzEam6ea3Kk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Keychron has also added a Hall Effect sensor to the B11 Pro, which means the keyboard automatically powers off when folded to preserve battery, and it turns back on and instantly reconnects to your laptop or phone when you unfold it. I love this as it negates the need to manually turn the B11 Pro off and back on again, and it worked effortlessly in my testing.</p><h2 id="great-build-quality-2">Great build 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="jTkz47X9hkf68fNvvcJJUk" name="Keychron_B11_Pro_Slimline_keyboard_008.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/jTkz47X9hkf68fNvvcJJUk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 because the Keychron B11 Pro doesn’t weigh a lot doesn’t mean you should underestimate the build quality. 95% of the time when I test a Keychron keyboard, I’m always impressed with the build, and that’s how I feel about the B11 Pro too. The keyboard is made entirely of ABS plastic but it doesn’t feel cheap or too plasticky. Tapping the keyboard with my fingernails didn’t reveal any hollow-sounding parts. It’s much, much better than the Seenda Foldable.</p><p>To give the B11 Pro a premium look, Keychron has fitted it with a soft-touch back cover. It’s finished with a matte black PU leather-like material which protects it from scratches, but also adds a touch of class to an otherwise unremarkable design. The construction and the low-key deep space gray design make the B11 Pro inconspicuous, helping it fit into the office and not draw any eyeballs when you’re, say, working on a train.</p><h2 id="good-typing-experience">Good typing experience</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZaFZPMRYyyjA8xktDWnmhk" name="Keychron_B11_Pro_Slimline_keyboard_010.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/ZaFZPMRYyyjA8xktDWnmhk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 accustomed to typing on a MacBook keyboard or prefer low-profile decks, the Keychron B11 Pro will surely please you. The B11 Pro features scissor switches with a short travel distance. This means the keyboard will register key presses without you having to apply a lot of force. The B11 Pro packs concave keycaps which feature a dip in the centre, so they cradle your fingertips and reduce typos.</p><p>Alongside offering a comfortable typing experience for hardcore typists, the B11 Pro offers a quiet sound profile too. The keys don’t sound thocky or clacky as some of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards </a>do, making the B11 Pro fit for the office. You won’t be forcing your coworkers to wear their <a href="https://www.tomsguide.com/us/best-noise-cancelling-headphones,review-5566.html">noise canceling headphones</a> all day long.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Keychron B11 Pro</p></td><td  ><p>96</p></td><td  ><p>92.01%</p></td></tr><tr><td class="firstcol " ><p>MacBook Air M2 built-in</p></td><td  ><p>91</p></td><td  ><p>92.06%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.42</p></td><td  ><p>91.14%</p></td></tr></tbody></table></div><p>To see how the B11 Pro fared against other keyboards I’ve tested, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, and you can see the results in the table above. I was able to achieve 96WPM with 92.01% accuracy, which is excellent. I will caveat this, though, by saying that this was my score after having used the keyboard for six hours. The B11 Pro poses a steep learning curve, which I’ll talk more about soon.</p><h2 id="web-based-software">Web-based software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/mg8JMG4jJCHpwfEEWHfVTZ-1920-80.jpg" alt="Keychron Launcher web-based app" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/mg7zqMtuSynRkebHfdg2UZ-1920-80.jpg" alt="Keychron Launcher web-based app" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure></figure><p>Similar to other Keychron keyboards, the Keychron B11 Pro is customizable via web-based software. You’ll need to access <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher</a> in a browser in order to personalize the keyboard. The software is clean, intuitive and user-friendly, and doesn’t feature intrusive pop-ups unlike bloatware (looking at you, Asus Armoury Crate).</p><p>Via Keychron Launcher, you can remap the B11 Pro’s keys, update or flash the firmware, change the polling rate (up to 1,000Hz), and even record macros. There’s no backlight customization as, well, the B11 Pro doesn’t feature any backlighting, and I’ll discuss this soon.</p><h2 id="very-affordable">Very affordable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kdecJah9Eib6NETFmSbSLk" name="Keychron_B11_Pro_Slimline_keyboard_006.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/kdecJah9Eib6NETFmSbSLk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 it’s a cheap but cheerful wireless keyboard you’re after, the Keychron B11 Pro is an excellent choice. Similar to its B-series stablemates, it provides a good typing experience without breaking the bank. The B11 Pro is available for <a href="https://www.keychron.com/products/keychron-b11-pro-ultra-slim-wireless-foldable-keyboard" target="_blank" rel="nofollow">$64 direct through Keychron</a>. While it’s a little pricier than the Seenda Foldable, it’s well worth the extra spend because the B11 Pro is better in nearly every way. It’s also much cheaper than the nimble<a href="https://www.tomsguide.com/computing/peripherals/logitech-mx-keys-mini-review"> Logitech MX Keys Mini</a> ($99),</p><p>Of course, if you don’t care much about the folding design and still want an ultra-slim keyboard, the Keychron B1 Pro and the B2 Pro are excellent alternatives — and both are about $20 cheaper than the B11 Pro.</p><h2 class="article-body__section" id="section-keychron-b11-pro-review-the-downs"><span>Keychron B11 Pro review: The downs</span></h2><p>While the Keychron B11 Pro is a great keyboard, it isn’t without its flaws. It isn’t particularly ergonomic, there’s a steep learning curve, and the keyboard doesn’t feature any backlighting.</p><h2 id="not-very-ergonomic">Not very ergonomic</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ndD7ZXJLgxHdNdiU7hiUyj" name="Keychron_B11_Pro_Slimline_keyboard_014.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/ndD7ZXJLgxHdNdiU7hiUyj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Keychron B11 Pro features an Alice layout which is split and slightly tilted for added comfort. This means that you don’t need to bend your wrists too much to type, as your wrists bend pretty much naturally when typing on the B11 Pro. While that’s all well and good, there aren’t many other ergonomic benefits offered by this keyboard. There are no feet to adjust the height, and the arrow keys especially feel too small and placed tightly together, so they’re a little uncomfortable.</p><p>I can understand why the B11 Pro doesn’t have any feet, in a way. Keychron clearly wanted to maintain a compact and lightweight form factor, and maybe it didn’t want to put feet into the PU leather cover. But I’d rather have adjustable feet, to be honest. While the Keychron B1 Pro and the B2 Pro don’t have adjustable feet, they have a bump on the top around the back, which props each keyboard up at a bit of an angle. It feels like a missed opportunity and, for me, undoes the ergo benefits of the Alice layout.</p><h2 id="can-present-a-steep-learning-curve">Can present a steep learning curve</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nsh3JVcfkDHZ9RjxxeAcqk" name="Keychron_B11_Pro_Slimline_keyboard_012.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/nsh3JVcfkDHZ9RjxxeAcqk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Keychron B11 Pro is a lovely keyboard to type on, and this is more of a pre-purchase condition than an outright flaw. Because the keyboard features an Alice layout, not every user will be familiar with it. For the first few hours of using the B11 Pro, I found myself looking down at the keyboard a fair amount. For context, here are my <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> typing test results: </p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Keychron B11 Pro (6 hours)</p></td><td  ><p>96</p></td><td  ><p>92.01%</p></td></tr><tr><td class="firstcol " ><p>Keychron B11 Pro (4 hours)</p></td><td  ><p>87</p></td><td  ><p>89.6%</p></td></tr><tr><td class="firstcol " ><p>Keychron B11 Pro (2 hours)</p></td><td  ><p>79</p></td><td  ><p>88.55%</p></td></tr><tr><td class="firstcol " ><p>Keychron B11 Pro (straight out of box)</p></td><td  ><p>69</p></td><td  ><p>96.3%</p></td></tr></tbody></table></div><p>I got off to a rocky start, as you can see in the table above. Straight out of the box, my type speed was abysmal on the B11 Pro, but my accuracy was the highest it’s been throughout the testing process. That’s because I was actually looking at the keys to type — even though I consider myself an excellent touch typist. My speeds improved as I went on, but my accuracy took a hit and then picked back up.</p><p>Don’t let this put you off. If you want a great typing experience, it’s worth powering through the initial lull. But of course, if you want to completely avoid that, I’d recommend grabbing a non-foldable but slim keyboard, like the Keychron B1 Pro, the B2 Pro, or the Logitech MX Keys Mini (if you can extend your budget that far).</p><h2 id="no-backlighting">No backlighting</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QLJyWZizXXVjEg5FGRGYQk" name="Keychron_B11_Pro_Slimline_keyboard_002.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/QLJyWZizXXVjEg5FGRGYQk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the Keychron B11 Pro doesn’t feature any backlighting, so the keys aren’t lit up. Now, if you’re a touch typist, that shouldn’t be an issue. But if you look at the keyboard when you type, as I did during the first few hours of the testing period, that’s going to be a problem. This is something I’ve seen with other slim keyboards too, such as the Keychron B1 Pro and the Seenda Foldable.</p><p>But the fact that the B11 Pro costs more than both those keyboards and <em>still </em>doesn’t feature backlighting is quite disappointing. The Logitech MX Keys Mini features white LED backlighting, if that’s something that matters a lot to you.</p><h2 class="article-body__section" id="section-keychron-b11-pro-review-verdict"><span>Keychron B11 Pro 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="dczBkyhtcEVXa6ZBRwtv9k" name="Keychron_B11_Pro_Slimline_keyboard_009.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/dczBkyhtcEVXa6ZBRwtv9k-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 on the hunt for a ridiculously slim keyboard for traveling and typing on the go, the Keychron B11 Pro should be your port of call. The B11 Pro’s scissor switches and concave keycaps are a joy to type on, and the keyboard isn’t very loud either, so you won’t draw unnecessary attention to yourself. I also love the web-based companion software and the fact that the B11 Pro is so affordable.</p><p>But if you aren’t down for getting over the initial learning curve presented by the keyboard’s Alice layout, you may want to consider alternatives. And while the Alice layout proves comfortable for your wrists, there aren’t many other ergonomic benefits the B11 Pro offers. Oh, and there’s no backlighting either, which is a shame.</p><p>None of these drawbacks matter to you, you say? Then the B11 Pro is perfect for you. But it also faces fierce competition from its stablemates, the B1 Pro and the B2 Pro, so choose wisely.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/keychron-b11-pro-review</link>
                                                                            <description>
                            <![CDATA[ The Keychron B11 Pro is a great foldable keyboard for all-day productivity and typing on the go, but it has considerable drawbacks. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">tZwLeM7gMbqGhYKfh7HWHW</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/DMBtrhAtgECG6QzTMKHjLk-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 01 May 2026 11:35:02 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:33 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/DMBtrhAtgECG6QzTMKHjLk-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Keychron B11 Pro foldable keyboard]]></media:description>                                                            <media:text><![CDATA[A black Keychron B11 Pro foldable keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A black Keychron B11 Pro foldable keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/DMBtrhAtgECG6QzTMKHjLk-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Keychron is no stranger to creating some of the <a href="https://www.tomsguide.com/best-picks/best-wireless-keyboard">best wireless keyboards</a> out there. In 2026, it decided to do something different and launch its first-ever folding keyboard. I’ve been testing the Keychron B11 Pro, and for the most part, it’s a delight. This is an ultra-slim keyboard that you can take on the go, or even slip into your pocket. Its scissor switches and concave keycaps offer a lovely typing experience, and customizing the keyboard is super easy too, thanks to web-based software.</p><p>But it’s far from perfect. Though it’s great to type on and its split Alice layout is comfortable for your wrists, there aren’t many other ergonomic benefits. There’s also a steep learning curve if you’re coming from traditional keyboards, and there’s no backlighting.</p><p>For the complete breakdown, read my full Keychron B11 Pro.</p><h2 class="article-body__section" id="section-keychron-b11-pro-review-specs"><span>Keychron B11 Pro review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.keychron.com/products/keychron-b11-pro-ultra-slim-wireless-foldable-keyboard" target="_blank" rel="nofollow">$64</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Scissor</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>ABS</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>65% Alice, 66 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows, Android, iOS, iPadOS</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>No</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz) | 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>250mAh rechargeable Li-polymer</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>15.45 x 5.62 x 0.2 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>9.1oz</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Deep Space Gray</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-keychron-b11-pro-review-cheat-sheet"><span>Keychron B11 Pro review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>An ultra-slim and portable productivity keyboard with scissor switches</li><li><strong>Who is it for?</strong> Anyone who travels a lot for work, or those on a budget</li><li><strong>How much does it cost? </strong>The Keychron B11 Pro is available for <a href="https://www.keychron.com/products/keychron-b11-pro-ultra-slim-wireless-foldable-keyboard" target="_blank" rel="nofollow">$64</a></li><li><strong>What do we like?</strong> The portable design, solid build quality, great typing experience, web-based software, and affordable price point</li><li><strong>What don’t we like? </strong>The non-ergonomic design, steep learning curve, and lack of backlighting</li></ul><h2 class="article-body__section" id="section-keychron-b11-pro-review-the-ups"><span>Keychron B11 Pro review: The ups</span></h2><p>The Keychron B11 Pro is extremely portable, and it offers a lovely typing experience. Oh, and customization is as easy as pie through web-based software, too.</p><h2 id="so-so-slim-and-portable">So, so slim and portable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8gvvhcjKMroztgTTucYzqk" name="Keychron_B11_Pro_Slimline_keyboard_004.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/8gvvhcjKMroztgTTucYzqk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Keychron is no stranger to creating keyboards that are highly portable. Just look at the <a href="https://www.tomsguide.com/computing/peripherals/keychron-b1-pro-review">Keychron B1 Pro</a> ($39) and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-b2-pro-review">B2 Pro</a> ($44). As part of the B-series, the Keychron B11 Pro is no different, but it has a neat trick up its sleeve: it can be folded to cut the size in half, making it <em>the </em>most portable keyboard I’ve tested so far.</p><p>When unfolded, the keyboard measures 15.45 x 5.62 x 0.2 inches, so you’re looking at about eight inches of space needed in your bag to stash the B11 Pro. It’s genius, really. The keyboard weighs next to nothing too, at 9.1oz. I’ve had no trouble carrying it in my backpack, which also included two boxed-up pairs of headphones, my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a>, and a few other small products. If you have big enough pockets, you’d probably be able to slip in the B11 Pro too. It’s more foldable and pocketable than its primary competition, the <a href="https://www.tomsguide.com/computing/peripherals/seenda-foldable-bluetooth-keyboard-with-touchpad-review">Seenda Foldable</a> ($46).</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="w9t4g436tW7HzEam6ea3Kk" name="Keychron_B11_Pro_Slimline_keyboard_011.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/w9t4g436tW7HzEam6ea3Kk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Keychron has also added a Hall Effect sensor to the B11 Pro, which means the keyboard automatically powers off when folded to preserve battery, and it turns back on and instantly reconnects to your laptop or phone when you unfold it. I love this as it negates the need to manually turn the B11 Pro off and back on again, and it worked effortlessly in my testing.</p><h2 id="great-build-quality-2">Great build 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="jTkz47X9hkf68fNvvcJJUk" name="Keychron_B11_Pro_Slimline_keyboard_008.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/jTkz47X9hkf68fNvvcJJUk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 because the Keychron B11 Pro doesn’t weigh a lot doesn’t mean you should underestimate the build quality. 95% of the time when I test a Keychron keyboard, I’m always impressed with the build, and that’s how I feel about the B11 Pro too. The keyboard is made entirely of ABS plastic but it doesn’t feel cheap or too plasticky. Tapping the keyboard with my fingernails didn’t reveal any hollow-sounding parts. It’s much, much better than the Seenda Foldable.</p><p>To give the B11 Pro a premium look, Keychron has fitted it with a soft-touch back cover. It’s finished with a matte black PU leather-like material which protects it from scratches, but also adds a touch of class to an otherwise unremarkable design. The construction and the low-key deep space gray design make the B11 Pro inconspicuous, helping it fit into the office and not draw any eyeballs when you’re, say, working on a train.</p><h2 id="good-typing-experience">Good typing experience</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ZaFZPMRYyyjA8xktDWnmhk" name="Keychron_B11_Pro_Slimline_keyboard_010.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/ZaFZPMRYyyjA8xktDWnmhk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 accustomed to typing on a MacBook keyboard or prefer low-profile decks, the Keychron B11 Pro will surely please you. The B11 Pro features scissor switches with a short travel distance. This means the keyboard will register key presses without you having to apply a lot of force. The B11 Pro packs concave keycaps which feature a dip in the centre, so they cradle your fingertips and reduce typos.</p><p>Alongside offering a comfortable typing experience for hardcore typists, the B11 Pro offers a quiet sound profile too. The keys don’t sound thocky or clacky as some of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards </a>do, making the B11 Pro fit for the office. You won’t be forcing your coworkers to wear their <a href="https://www.tomsguide.com/us/best-noise-cancelling-headphones,review-5566.html">noise canceling headphones</a> all day long.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Keychron B11 Pro</p></td><td  ><p>96</p></td><td  ><p>92.01%</p></td></tr><tr><td class="firstcol " ><p>MacBook Air M2 built-in</p></td><td  ><p>91</p></td><td  ><p>92.06%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a></p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.42</p></td><td  ><p>91.14%</p></td></tr></tbody></table></div><p>To see how the B11 Pro fared against other keyboards I’ve tested, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, and you can see the results in the table above. I was able to achieve 96WPM with 92.01% accuracy, which is excellent. I will caveat this, though, by saying that this was my score after having used the keyboard for six hours. The B11 Pro poses a steep learning curve, which I’ll talk more about soon.</p><h2 id="web-based-software">Web-based software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/mg8JMG4jJCHpwfEEWHfVTZ-1920-80.jpg" alt="Keychron Launcher web-based app" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/mg7zqMtuSynRkebHfdg2UZ-1920-80.jpg" alt="Keychron Launcher web-based app" /><figcaption><small role="credit">Keychron / Tom's Guide</small></figcaption></figure></figure><p>Similar to other Keychron keyboards, the Keychron B11 Pro is customizable via web-based software. You’ll need to access <a href="https://launcher.keychron.com/" target="_blank" rel="nofollow">Keychron Launcher</a> in a browser in order to personalize the keyboard. The software is clean, intuitive and user-friendly, and doesn’t feature intrusive pop-ups unlike bloatware (looking at you, Asus Armoury Crate).</p><p>Via Keychron Launcher, you can remap the B11 Pro’s keys, update or flash the firmware, change the polling rate (up to 1,000Hz), and even record macros. There’s no backlight customization as, well, the B11 Pro doesn’t feature any backlighting, and I’ll discuss this soon.</p><h2 id="very-affordable">Very affordable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="kdecJah9Eib6NETFmSbSLk" name="Keychron_B11_Pro_Slimline_keyboard_006.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/kdecJah9Eib6NETFmSbSLk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 it’s a cheap but cheerful wireless keyboard you’re after, the Keychron B11 Pro is an excellent choice. Similar to its B-series stablemates, it provides a good typing experience without breaking the bank. The B11 Pro is available for <a href="https://www.keychron.com/products/keychron-b11-pro-ultra-slim-wireless-foldable-keyboard" target="_blank" rel="nofollow">$64 direct through Keychron</a>. While it’s a little pricier than the Seenda Foldable, it’s well worth the extra spend because the B11 Pro is better in nearly every way. It’s also much cheaper than the nimble<a href="https://www.tomsguide.com/computing/peripherals/logitech-mx-keys-mini-review"> Logitech MX Keys Mini</a> ($99),</p><p>Of course, if you don’t care much about the folding design and still want an ultra-slim keyboard, the Keychron B1 Pro and the B2 Pro are excellent alternatives — and both are about $20 cheaper than the B11 Pro.</p><h2 class="article-body__section" id="section-keychron-b11-pro-review-the-downs"><span>Keychron B11 Pro review: The downs</span></h2><p>While the Keychron B11 Pro is a great keyboard, it isn’t without its flaws. It isn’t particularly ergonomic, there’s a steep learning curve, and the keyboard doesn’t feature any backlighting.</p><h2 id="not-very-ergonomic">Not very ergonomic</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="ndD7ZXJLgxHdNdiU7hiUyj" name="Keychron_B11_Pro_Slimline_keyboard_014.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/ndD7ZXJLgxHdNdiU7hiUyj-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Keychron B11 Pro features an Alice layout which is split and slightly tilted for added comfort. This means that you don’t need to bend your wrists too much to type, as your wrists bend pretty much naturally when typing on the B11 Pro. While that’s all well and good, there aren’t many other ergonomic benefits offered by this keyboard. There are no feet to adjust the height, and the arrow keys especially feel too small and placed tightly together, so they’re a little uncomfortable.</p><p>I can understand why the B11 Pro doesn’t have any feet, in a way. Keychron clearly wanted to maintain a compact and lightweight form factor, and maybe it didn’t want to put feet into the PU leather cover. But I’d rather have adjustable feet, to be honest. While the Keychron B1 Pro and the B2 Pro don’t have adjustable feet, they have a bump on the top around the back, which props each keyboard up at a bit of an angle. It feels like a missed opportunity and, for me, undoes the ergo benefits of the Alice layout.</p><h2 id="can-present-a-steep-learning-curve">Can present a steep learning curve</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="nsh3JVcfkDHZ9RjxxeAcqk" name="Keychron_B11_Pro_Slimline_keyboard_012.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/nsh3JVcfkDHZ9RjxxeAcqk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Keychron B11 Pro is a lovely keyboard to type on, and this is more of a pre-purchase condition than an outright flaw. Because the keyboard features an Alice layout, not every user will be familiar with it. For the first few hours of using the B11 Pro, I found myself looking down at the keyboard a fair amount. For context, here are my <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a> typing test results: </p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Keychron B11 Pro (6 hours)</p></td><td  ><p>96</p></td><td  ><p>92.01%</p></td></tr><tr><td class="firstcol " ><p>Keychron B11 Pro (4 hours)</p></td><td  ><p>87</p></td><td  ><p>89.6%</p></td></tr><tr><td class="firstcol " ><p>Keychron B11 Pro (2 hours)</p></td><td  ><p>79</p></td><td  ><p>88.55%</p></td></tr><tr><td class="firstcol " ><p>Keychron B11 Pro (straight out of box)</p></td><td  ><p>69</p></td><td  ><p>96.3%</p></td></tr></tbody></table></div><p>I got off to a rocky start, as you can see in the table above. Straight out of the box, my type speed was abysmal on the B11 Pro, but my accuracy was the highest it’s been throughout the testing process. That’s because I was actually looking at the keys to type — even though I consider myself an excellent touch typist. My speeds improved as I went on, but my accuracy took a hit and then picked back up.</p><p>Don’t let this put you off. If you want a great typing experience, it’s worth powering through the initial lull. But of course, if you want to completely avoid that, I’d recommend grabbing a non-foldable but slim keyboard, like the Keychron B1 Pro, the B2 Pro, or the Logitech MX Keys Mini (if you can extend your budget that far).</p><h2 id="no-backlighting">No backlighting</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="QLJyWZizXXVjEg5FGRGYQk" name="Keychron_B11_Pro_Slimline_keyboard_002.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/QLJyWZizXXVjEg5FGRGYQk-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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, the Keychron B11 Pro doesn’t feature any backlighting, so the keys aren’t lit up. Now, if you’re a touch typist, that shouldn’t be an issue. But if you look at the keyboard when you type, as I did during the first few hours of the testing period, that’s going to be a problem. This is something I’ve seen with other slim keyboards too, such as the Keychron B1 Pro and the Seenda Foldable.</p><p>But the fact that the B11 Pro costs more than both those keyboards and <em>still </em>doesn’t feature backlighting is quite disappointing. The Logitech MX Keys Mini features white LED backlighting, if that’s something that matters a lot to you.</p><h2 class="article-body__section" id="section-keychron-b11-pro-review-verdict"><span>Keychron B11 Pro 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="dczBkyhtcEVXa6ZBRwtv9k" name="Keychron_B11_Pro_Slimline_keyboard_009.JPG" alt="A black Keychron B11 Pro foldable keyboard" src="https://cdn.mos.cms.futurecdn.net/dczBkyhtcEVXa6ZBRwtv9k-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 on the hunt for a ridiculously slim keyboard for traveling and typing on the go, the Keychron B11 Pro should be your port of call. The B11 Pro’s scissor switches and concave keycaps are a joy to type on, and the keyboard isn’t very loud either, so you won’t draw unnecessary attention to yourself. I also love the web-based companion software and the fact that the B11 Pro is so affordable.</p><p>But if you aren’t down for getting over the initial learning curve presented by the keyboard’s Alice layout, you may want to consider alternatives. And while the Alice layout proves comfortable for your wrists, there aren’t many other ergonomic benefits the B11 Pro offers. Oh, and there’s no backlighting either, which is a shame.</p><p>None of these drawbacks matter to you, you say? Then the B11 Pro is perfect for you. But it also faces fierce competition from its stablemates, the B1 Pro and the B2 Pro, so choose wisely.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ Lofree Hyzen early access review: What the YouTubers aren't telling you ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I admire Lofree for making the Hyzen, the brand’s new dual mechanical/magnetic keyboard. Dual-socket boards are ambitious enough in concept, let alone full scale production. So it’s a brave move from a manufacturer consistently struggling with its firmware, software and reliability.</p><p>However… first some context. The Lofree Hyzen isn’t the first <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">gaming keyboard</a> with a dual mechanical/magnetic switch socket. Glorious got there already with the socket on the <a href="https://www.tomsguide.com/computing/peripherals/glorious-gmmk-3-he-review">GMMK 3 HE</a> ($199). Lofree has gone a step further by designing (with Kailh) a dual MX/TMR switch to go in its clever socket.</p><p>Thus, claims Lofree, the Hyzen is a huge step forward, bundling into a single package both the typing performance of mechanicals and the gaming performance of magnetic switches. Now, I don’t think it’s as big a step as Lofree thinks it is, but the brand <em>is</em> onto something. And I think the Hyzen could herald some interesting possibilities ahead.</p><p>Unfortunately, while this board is in Kickstarter/pre-production, I can’t recommend you pre-order it. Both my Hyzen review samples suffer the usual Lofree problems: janky software and firmware, questionable quality control and a high price (the Hyzen is especially expensive, even for Lofree). </p><p>Almost all the other coverage I've seen about this board, especially on YouTube, seems overwhelmingly positive, and that just isn't the experience I've had so far. Find out more in my early Lofree Hyzen review.</p><p><em><strong>April 28th 2026: </strong></em><em>Many of my issues may be explained by my units being pre-production models, which is why I’ve left this review un-scored for now. I’ve also reached out to Lofree to figure out whether these are known issues and if they’ll be fixed by full production. But when people are being encouraged to invest their money into a Kickstarter campaign, it’s my duty as a tech journalist to tell you all what I actually see in front of me.</em></p><h2 class="article-body__section" id="section-lofree-hyzen-review-cheat-sheet"><span>Lofree Hyzen review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A hybrid productivity/gaming keyboard with mechanical and magnetic switch compatibility</li><li><strong>Who is it for?</strong> People who love the feel of MX switches but want the gaming performance of magnetic</li><li><strong>What does it cost?</strong> It's currently in <a href="https://www.kickstarter.com/projects/lofree/hyzen-worlds-first-mechanical-magnetic-keyboard" target="_blank" rel="nofollow">Kickstarter</a>, but will cost $279 when launched</li><li><strong>What's good?</strong> The dual compatibility, styling, typing performance and gaming features</li><li><strong>What isn't?</strong> The number of QC issues on our units, the janky software, switch issues and high MSRP.</li></ul><h2 class="article-body__section" id="section-lofree-hyzen-review-specs"><span>Lofree Hyzen review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p>Currently in Kickstarter, MSRP will be $279 (Tri-Mode)</p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Kailh Nexus TMR</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Doubleshot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum alloy</p></td></tr><tr><td class="firstcol " ><p><strong>Mount type</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>65%, 67-key</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS, iOS, iPadOS, Android</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired/USB-C/Bluetooth</p></td></tr><tr><td class="firstcol " ><p><strong>Battery capacity</strong></p></td><td  ><p>10,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>13 x 5.6 x 1.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>4.1lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Silver; Gray</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-lofree-hyzen-review-the-ups"><span>Lofree Hyzen review: The ups</span></h2><p>A dual compatible socket plus an innovative switch; gorgeous design and effortless typing — the Hyzen has a lot going for it.</p><h2 id="dual-mx-magnetic-compatibility">Dual MX/magnetic compatibility</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wgq3A4tiV3DK5BvHRMxdJK" name="Lofree_Hyzen_ 11.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/wgq3A4tiV3DK5BvHRMxdJK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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’ll start off with the obvious: the Hyzen is compatible with both mechanical and magnetic switches. Its socket supports 3- or 5-pin mechanicals, so you can fit your favorite tactile or clicky switches. It also supports magnetic switches, and a decent variety of them, too. The Hyzen is compatible with the stock Kailh Nexus switches; Gateron’s Magnetic, Jade and Jade Pro lines; TTC’s Magneto switches; and more. All you need to do is recalibrate the board once you’ve fitted compatible switches.</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="FfHzULLzD6mwQwyaroBMnZ" name="Lofree Hyzen Screenshots-3" alt="A screenshot of the Lofree Web Hub being used to customize the Lofree Hyzen" src="https://cdn.mos.cms.futurecdn.net/FfHzULLzD6mwQwyaroBMnZ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/FfHzULLzD6mwQwyaroBMnZ-1920-80.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">The switch selection panel in the Lofree Web Hub companion app. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Lofree / Future)</span></figcaption></figure><p>This is one of the most important points for me: many magnetic boards, like the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review"><u>Keychron K2 HE</u></a> ($130), are compatible only with their stock switches. That means anywhere from 1-3 hot swap choices and little variation between those choices save spring weights. The Hyzen isn’t the first to expand on this, but it’s great to finally see wider compatibility coming to the market.</p><h2 id="innovative-kailh-nexus-switch">Innovative Kailh Nexus switch</h2><p>Lofree didn’t just leave it at the socket, though. The stock Kailh Nexus switch (which Lofree co-designed with Kailh for this deck) is also dual MX/Mag in operation. Set it to mechanical mode in the companion software and the switch behaves like a mechanical switch, using contact-based actuation — you can see the mechanical pins sticking out the bottom of the housing! </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="2VtErCNDp3vxy5BujYcoPK" name="Lofree_Hyzen_ 5.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/2VtErCNDp3vxy5BujYcoPK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Set it to magnetic, and the switch actuates based on magnetic positioning. In theory, this gives you the best of both worlds. The feel of mechanical with the features of magnetic. It’s a little pointless, though. It’s only the actuation detection that changes between the modes. The switches feel the same in either mode, so you might as well always leave them in Magnetic mode and choose the actuation distance you want.</p><p>They’re very nice to use, though — thanks to the moving stem and contact plate, they feel just like a traditional linear switch. And with a modest 40g actuation force, they’re fairly light and airy. Still, they don’t feel <em>that</em> different to standard linear magnetic switches. </p><p>Most importantly, I'm excited to see how this technology can be used in more switches going forward! So kudos to Lofree and Kailh for doing something new.</p><h2 id="plenty-of-gaming-features">Plenty of gaming 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="FomysNB3Q2w3ykmpcHw9hJ" name="Lofree_Hyzen_ 6.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/FomysNB3Q2w3ykmpcHw9hJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>In their magnetic mode, the Nexus switches are great for gaming. You'll find plenty of the zeitgeisty magnetic gaming features we’ve come to expect from today's high end gaming boards — although not quite as many as you'll find on boards like the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air60-he-tiny-profile-huge-performance">NuPhy Air60 HE</a> ($139) and <a href="https://www.tomsguide.com/computing/peripherals/nuphy-field75-he-review">Field75 HE</a> ($169), or the <a href="https://www.tomsguide.com/computing/keyboards/lemokey-p2-he-review-a-tiny-powerhouse">Lemokey P2 HE</a> ($169) and its gamepad analog mode.</p><p>Still, the Hyzen has the big hitters like adjustable actuation, Rapid Trigger, Last Key Prioritization SOCD (A.K.A ‘Snap Tap’), and more. All of this is controlled through the Lofree Web Hub, which I’ll discuss in more detail below.</p><p>I lowered the switch actuation down to around 0.5mm for Counter Strike 2 — quick enough to be extremely responsive without running the risk of accidental inputs. I would normally drop lower, to around 0.3mm, but as I’ll cover later, the Hyzen had significant issues with accidental and repeated inputs at the lowest actuation distances. So I needed a decent (0.3mm) dead zone.</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:1720px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="gjEjb8DAKA9LonPq2rwVjZ" name="Lofree Hyzen Screenshots-2" alt="A screenshot of the Lofree Web Hub being used to customize the Lofree Hyzen" src="https://cdn.mos.cms.futurecdn.net/gjEjb8DAKA9LonPq2rwVjZ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1720" height="968" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/gjEjb8DAKA9LonPq2rwVjZ-1920-80.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: Lofree / Future)</span></figcaption></figure><p>Still, at 0.5mm, the Nexus switches were super snappy and responsive, and a joy to use in FPS titles like CS2 and Isonzo. For less intense games like Kingdom Come Deliverance 2 and Cities: Skylines 2, I scaled the actuation back to my preferred typing distance of 2.6mm.</p><p>The Hyzen also polls at 8K, for super low latency. In real terms, you won’t notice a difference between a 1K board and an 8K board in terms of latency — 8K is essentially marketing snake oil. But hey, 8K is technically faster than 1K.</p><h2 id="a-mean-productivity-machine">A mean productivity machine</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tfaApM2oLVpDpVey6JyemJ" name="Lofree_Hyzen_ 4.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/tfaApM2oLVpDpVey6JyemJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Hyzen is a serious productivity machine, especially for a 65% board. There’s a neat toggle on the left which turns the number row into Fn buttons, giving you access to productivity shortcuts — which I find especially handy when using macOS at work.</p><p>As I’ll cover later, I had some major input registry issues in mechanical switch mode, but in magnetic mode at 2.6mm actuation and with a hefty amount of dead zone dialled in, I was on <em><strong>fire</strong></em> in the type test. I managed to type at 105WPM with 93% accuracy. That’s 10WPM above my average speed at nearly 5 percentage points above my average accuracy. </p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Words per minute</strong></p></th><th  ><p><strong>Typing accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Lofree Hyzen</p></td><td  ><p>105</p></td><td  ><p>93%</p></td></tr><tr><td class="firstcol " ><p>Lemokey P2 HE</p></td><td  ><p>100</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a></p></td><td  ><p>99</p></td><td  ><p>92%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/gaming/gaming-peripherals/corsair-vanguard-pro-96-review">Corsair Vanguard Pro 96</a></p></td><td  ><p>105</p></td><td  ><p>96%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/wooting-80he-review">Wooting 80HE</a></p></td><td  ><p>95</p></td><td  ><p>87%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/lemokey-l5-he-review">Lemokey L5 HE</a></p></td><td  ><p>104</p></td><td  ><p>87%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/keychron-q16-he-8k-review">Keychron Q16 HE 8K</a></p></td><td  ><p>99</p></td><td  ><p>92%</p></td></tr><tr><td class="firstcol " ><p>NuPhy Air60 HE</p></td><td  ><p>101</p></td><td  ><p>88%</p></td></tr><tr><td class="firstcol " ><p>Keychron K2 HE</p></td><td  ><p>106</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p>Pete's rolling averages (all keyboards)</p></td><td  ><p>95.23</p></td><td  ><p>88.34%</p></td></tr></tbody></table></div><p>This is all helped by lovely keycaps. They’re Cherry profile — nothing special there — but finished with a beautiful friction-heavy coating that a) feels great, and b) helps reduce slipping and mistypes.</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="VTJixAk9Ar8FSJwvnELUQJ" name="Lofree_Hyzen_ 7.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/VTJixAk9Ar8FSJwvnELUQJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">As you can see, there's a very steep overall angle to the board. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The keys have a very modest rake, but thanks to a 12-degree rearward pitch angle, the keyboard still feels very steep, and each row is noticeably tiered above the one below. I didn’t get on with this at first, as I’m not accustomed to such a stark difference in the upper- and lower-most key heights. But I got used to it, and it clearly didn’t negatively affect my speed or accuracy! </p><h2 id="captivating-construction">Captivating construction</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FH8yLdyAztir9JhQk26UFK" name="Lofree_Hyzen_ 1.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/FH8yLdyAztir9JhQk26UFK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Hyzen is utterly gorgeous to behold. Although I expect nothing else from Lofree. The clean, straight lines; the translucent keycaps; the backlight and desklight; the black digital Fn bar. It’s just so slick. Lofree sent me both the gray and white versions, and I’ve had a really hard time choosing which I prefer. Each look fantastic with the RGB backlighting set to a simplistic white, and the desklight set to red. </p><p>Like the <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-2-review">Lofree Flow 2</a> ($149) and <a href="https://www.tomsguide.com/computing/peripherals/lofree-flow84-review-beautiful-but-flawed">Lofree Flow</a> ($159), the Hyzen utilizes a minimalist design ethos. Here, it’s a deliberate attempt to draw focus to the quality of materials used. And they are indeed high quality — thick, machined aluminum alloy for the case; doubleshot PBT keycaps; and an FR4 fiberglass plate. It feels like a premium product.</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="NSzhehZbdzQxJezsUjgMdJ" name="Lofree_Hyzen_ 9.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/NSzhehZbdzQxJezsUjgMdJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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><strong>April 28th 2026: </strong>My units are both early production samples, and a few changes will be made to the appearance of final production units. These include the removal of the (ugly, wonkily-printed) “Hyzen” logo on the space bar; and the addition of macOS legends to keycaps, resulting in dual-labelled caps.</p><h2 id="a-more-ish-muted-clack">A more-ish muted clack</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2MT2dCdgxiPsNZhMdwnGNK" name="Lofree_Hyzen_ 3.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/2MT2dCdgxiPsNZhMdwnGNK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Hyzen has a rather more-ish sound. Thanks to the FR4 plate and PBT caps, there’s a muted, plasticky-but-pleasing clack. It isn’t addictive as the poppy <a href="https://www.tomsguide.com/computing/peripherals/wobkey-rainy-75-pro-review">Wobkey Rainy 75 Pro</a> ($139) or the thocky <a href="https://www.tomsguide.com/computing/peripherals/nuphy-halo-75-96-v2-review">NuPhy Halo75 V2</a> ($129), but still rather pleasant.</p><p>Thanks to that heavy aluminum alloy case and a decent amount of internal damping, the Hyzen’s sound is nice and tight, with little in the way of echoes or ricochets. </p><p>Admittedly, my gray sample has an <em>extremely</em> noisy Space bar stabilizer, which generates a metallic ping each time you hit the key. But I’ll cover this more later on.</p><h2 class="article-body__section" id="section-lofree-hyzen-review-the-downs"><span>Lofree Hyzen review: The downs</span></h2><p>Unfortunately, my review units demonstrated poor QC, janky software and switch issues. Then there’s the steep MSRP to consider. The Hyzen is too much of a risk right now for me to recommend.</p><h2 id="poor-quality-control">Poor quality control</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8pP3Fs7JYZMjJnoFMNAwSK" name="Lofree_Hyzen_ 12.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/8pP3Fs7JYZMjJnoFMNAwSK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Unfortunately, my pre-production units suffer from poor quality control. One of my Hyzens likes to rattle — specifically, it has a very noisy, metallic ping from the Space bar stabilizers. It’s a shame because the other unit has a fairly nice sounding Space bar that Lofree has managed to sound-match fairly well with the rest of the keys (within reason, as it’s still a Space bar).</p><p>This isn’t surprising given Lofree’s recent form. Both of my Lofree Flow 2 testers, for example, demonstrated a truly vibe-killing metallic ricochet — a common problem that Lofree has acknowledged to me as QC-related.</p><p>A week or so into testing, and with the battery fully charged, one of my test samples then decided to randomly die... before turning back on 30 minutes later. Plus, the zips on both of my test unit cases were far too small, meaning they caught on the zip fabric and both broke — we had to cut one of the cases open to get to the keyboard (Lofree has confirmed it will revise the zips for production models).</p><h2 id="slightly-janky-software">Slightly janky software</h2><p>With mechanical boards, you can get away with simple open source firmware/software like QMK/VIA. But with magnetic boards, whose core performance relies so much on software customization, the open source platforms just can’t cut it. That’s when you need a solid web app like Keychron’s/Lemokey’s Launcher or NuPhy.io. </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:3200px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4U4YvCNFqh5Xu5N3J9YwzZ" name="Lofree Hyzen Screenshots-1" alt="A screenshot of the Lofree Web Hub being used to customize the Lofree Hyzen" src="https://cdn.mos.cms.futurecdn.net/4U4YvCNFqh5Xu5N3J9YwzZ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="3200" height="1800" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/4U4YvCNFqh5Xu5N3J9YwzZ-1920-80.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: Lofree / Future)</span></figcaption></figure><p>Lofree Hub will be Lofree’s version of those. Right now at least, it works, but it ain’t polished. My early access version was very janky, taking literal minutes to load the background image (like dial up) and buttons, then lagging in use. </p><p>Firmware updating was also pretty clunky, requiring me to download a standalone .EXE (so no updating on Mac) rather than simply updating through the web app, as with Keychron and NuPhy boards.</p><p>Hopefully, the final production software will be an improvement on this. But Lofree’s previous proprietary software platforms have been equally janky, and usually get canned by the manufacturer after one or two boards (see my <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-lite84-review">Lofree Flow Lite review</a>). So, again with Lofree's recent form in mind, forgive me if I’m not full of optimism here.</p><h2 id="dead-zone-required-and-no-mx-bounce-control">Dead zone required and no MX bounce control</h2><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="2MT2dCdgxiPsNZhMdwnGNK" name="Lofree_Hyzen_ 3.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:215,l:395,cw:960,ch:540,q:80/2MT2dCdgxiPsNZhMdwnGNK.jpg" mos="" align="middle" fullscreen="" width="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 found the Nexus switches in MX mode difficult to use. Despite flawless performance in magnetic mode, almost setting my PBs, I could only just type 90 words per minute  in MX mode at only 87% accuracy. I had repeated/double inputs all over the shop, even after recalibration. MX mode is just crying out for some bounce/debounce settings to iron this out, but the app gives no control over that. I’ve just left the thing in magnetic mode and have reached out to Lofree about the input issues.</p><p>In magnetic mode, there were a lot of accidental inputs at short actuation distances. This is normal (although I’ve never had as many issues as with the Hyzen/Nexus switches), and as I mentioned above, I just dialled in 0.3mm of dead zone to fix the issue at 0.4mm and 0.5mm actuation.</p><p><strong>April 28th 2026: </strong>Lofree has confirmed to me that MX switch issues are known about in pre-production units.</p><h2 id="astronomically-high-price">Astronomically high price</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tfaApM2oLVpDpVey6JyemJ" name="Lofree_Hyzen_ 4.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/tfaApM2oLVpDpVey6JyemJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Finally, there’s the Hyzen’s high price. There will be some deals for Kickstarter supporters, but the MSRP of the tri-mode variant will be $279. That’s crazy steep — even pricier than high-end magnetic boards like the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review"><u>Keychron Q6 8K HE</u></a> ($239) and <a href="https://www.tomsguide.com/computing/peripherals/lemokey-l5-he-review"><u>Lemokey L5 HE</u></a> ($220).</p><p>Despite its promising features, given the number of issues I've experienced, I don't think this board warrants its price. If you're gonna charge that much for a deck, it better be polished. Again, the product is in Kickstarter, so the final production model may have these issues ironed out, but there's no guarantee of that, so I can only go on what I see in front of me.</p><p>Naturally, with Kickstarter there's always the possibility of rug pulls, too. Lofree is an established brand (why use Kickstarter, then?), so I doubt that will happen here, but I'll always advise extreme caution when it comes to crowdfunding projects — that's especially true here given the Hyzen is so clearly unfinished.</p><h2 class="article-body__section" id="section-lofree-hyzen-review-verdict"><span>Lofree Hyzen 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="eBP3vNsqCkLjeMHFw2ppGK" name="Lofree_Hyzen_ 10.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/eBP3vNsqCkLjeMHFw2ppGK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 love seeing keyboard manufacturers take risks and experiment. Which is why I was so excited about the Hyzen. A dual MX/Mag socket and switch combo has the potential to be the future default setup. In fact, I think it definitely will be. So kudos to Lofree for becoming an early adopter (I’ve no doubt the other big productivity brands will be following suit soon).</p><p>Unfortunately, I can’t recommend the Hyzen in its current form. It’s beautiful to behold, great to type on, sounds pretty good and offers a range of gaming features. But it’s let down by poor QC, plus janky software and switches. I’m really hoping Lofree can iron out these issues before launch. But then oh my lord there’s still <em><strong>that price</strong></em>.</p><p>Lofree: if you’re going to experiment, I’ll back you all the way. But don’t think you can get away with charging <em>double</em> finished-product prices while doing so, for a product that isn’t anywhere near ready for market.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/lofree-hyzen-review</link>
                                                                            <description>
                            <![CDATA[ I tested the Lofree Hyzen in early access — here's what I think about this dashing new dual-compatible MX/magnetic keyboard. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">KxUnf3nURhNiM9BAcon2Lc</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/xzLfucCQAtKVmgqJUZimVK-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 28 Apr 2026 11:26:04 +0000</pubDate>                                                                                                                                <updated>Tue, 28 Apr 2026 14:04:06 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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-320-70.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/xzLfucCQAtKVmgqJUZimVK-1920-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[The Lofree Hyzen keyboard on a stone surface]]></media:description>                                                            <media:text><![CDATA[The Lofree Hyzen keyboard on a stone surface]]></media:text>
                                <media:title type="plain"><![CDATA[The Lofree Hyzen keyboard on a stone surface]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/xzLfucCQAtKVmgqJUZimVK-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I admire Lofree for making the Hyzen, the brand’s new dual mechanical/magnetic keyboard. Dual-socket boards are ambitious enough in concept, let alone full scale production. So it’s a brave move from a manufacturer consistently struggling with its firmware, software and reliability.</p><p>However… first some context. The Lofree Hyzen isn’t the first <a href="https://www.tomsguide.com/best-picks/best-gaming-keyboard">gaming keyboard</a> with a dual mechanical/magnetic switch socket. Glorious got there already with the socket on the <a href="https://www.tomsguide.com/computing/peripherals/glorious-gmmk-3-he-review">GMMK 3 HE</a> ($199). Lofree has gone a step further by designing (with Kailh) a dual MX/TMR switch to go in its clever socket.</p><p>Thus, claims Lofree, the Hyzen is a huge step forward, bundling into a single package both the typing performance of mechanicals and the gaming performance of magnetic switches. Now, I don’t think it’s as big a step as Lofree thinks it is, but the brand <em>is</em> onto something. And I think the Hyzen could herald some interesting possibilities ahead.</p><p>Unfortunately, while this board is in Kickstarter/pre-production, I can’t recommend you pre-order it. Both my Hyzen review samples suffer the usual Lofree problems: janky software and firmware, questionable quality control and a high price (the Hyzen is especially expensive, even for Lofree). </p><p>Almost all the other coverage I've seen about this board, especially on YouTube, seems overwhelmingly positive, and that just isn't the experience I've had so far. Find out more in my early Lofree Hyzen review.</p><p><em><strong>April 28th 2026: </strong></em><em>Many of my issues may be explained by my units being pre-production models, which is why I’ve left this review un-scored for now. I’ve also reached out to Lofree to figure out whether these are known issues and if they’ll be fixed by full production. But when people are being encouraged to invest their money into a Kickstarter campaign, it’s my duty as a tech journalist to tell you all what I actually see in front of me.</em></p><h2 class="article-body__section" id="section-lofree-hyzen-review-cheat-sheet"><span>Lofree Hyzen review: Cheat sheet</span></h2><ul><li><strong>What is it?</strong> A hybrid productivity/gaming keyboard with mechanical and magnetic switch compatibility</li><li><strong>Who is it for?</strong> People who love the feel of MX switches but want the gaming performance of magnetic</li><li><strong>What does it cost?</strong> It's currently in <a href="https://www.kickstarter.com/projects/lofree/hyzen-worlds-first-mechanical-magnetic-keyboard" target="_blank" rel="nofollow">Kickstarter</a>, but will cost $279 when launched</li><li><strong>What's good?</strong> The dual compatibility, styling, typing performance and gaming features</li><li><strong>What isn't?</strong> The number of QC issues on our units, the janky software, switch issues and high MSRP.</li></ul><h2 class="article-body__section" id="section-lofree-hyzen-review-specs"><span>Lofree Hyzen review: Specs</span></h2><div ><table><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p>Currently in Kickstarter, MSRP will be $279 (Tri-Mode)</p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Kailh Nexus TMR</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Doubleshot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum alloy</p></td></tr><tr><td class="firstcol " ><p><strong>Mount type</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>65%, 67-key</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>Windows, macOS, iOS, iPadOS, Android</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>8,000Hz</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired/USB-C/Bluetooth</p></td></tr><tr><td class="firstcol " ><p><strong>Battery capacity</strong></p></td><td  ><p>10,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>13 x 5.6 x 1.9 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>4.1lbs</p></td></tr><tr><td class="firstcol " ><p><strong>Colors</strong></p></td><td  ><p>Silver; Gray</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-lofree-hyzen-review-the-ups"><span>Lofree Hyzen review: The ups</span></h2><p>A dual compatible socket plus an innovative switch; gorgeous design and effortless typing — the Hyzen has a lot going for it.</p><h2 id="dual-mx-magnetic-compatibility">Dual MX/magnetic compatibility</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wgq3A4tiV3DK5BvHRMxdJK" name="Lofree_Hyzen_ 11.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/wgq3A4tiV3DK5BvHRMxdJK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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’ll start off with the obvious: the Hyzen is compatible with both mechanical and magnetic switches. Its socket supports 3- or 5-pin mechanicals, so you can fit your favorite tactile or clicky switches. It also supports magnetic switches, and a decent variety of them, too. The Hyzen is compatible with the stock Kailh Nexus switches; Gateron’s Magnetic, Jade and Jade Pro lines; TTC’s Magneto switches; and more. All you need to do is recalibrate the board once you’ve fitted compatible switches.</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="FfHzULLzD6mwQwyaroBMnZ" name="Lofree Hyzen Screenshots-3" alt="A screenshot of the Lofree Web Hub being used to customize the Lofree Hyzen" src="https://cdn.mos.cms.futurecdn.net/FfHzULLzD6mwQwyaroBMnZ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1600" height="900" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/FfHzULLzD6mwQwyaroBMnZ-1920-80.jpg' target='_blank' class='expand-button icon-expand-image icon' ></a></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">The switch selection panel in the Lofree Web Hub companion app. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Lofree / Future)</span></figcaption></figure><p>This is one of the most important points for me: many magnetic boards, like the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review"><u>Keychron K2 HE</u></a> ($130), are compatible only with their stock switches. That means anywhere from 1-3 hot swap choices and little variation between those choices save spring weights. The Hyzen isn’t the first to expand on this, but it’s great to finally see wider compatibility coming to the market.</p><h2 id="innovative-kailh-nexus-switch">Innovative Kailh Nexus switch</h2><p>Lofree didn’t just leave it at the socket, though. The stock Kailh Nexus switch (which Lofree co-designed with Kailh for this deck) is also dual MX/Mag in operation. Set it to mechanical mode in the companion software and the switch behaves like a mechanical switch, using contact-based actuation — you can see the mechanical pins sticking out the bottom of the housing! </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="2VtErCNDp3vxy5BujYcoPK" name="Lofree_Hyzen_ 5.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/2VtErCNDp3vxy5BujYcoPK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Set it to magnetic, and the switch actuates based on magnetic positioning. In theory, this gives you the best of both worlds. The feel of mechanical with the features of magnetic. It’s a little pointless, though. It’s only the actuation detection that changes between the modes. The switches feel the same in either mode, so you might as well always leave them in Magnetic mode and choose the actuation distance you want.</p><p>They’re very nice to use, though — thanks to the moving stem and contact plate, they feel just like a traditional linear switch. And with a modest 40g actuation force, they’re fairly light and airy. Still, they don’t feel <em>that</em> different to standard linear magnetic switches. </p><p>Most importantly, I'm excited to see how this technology can be used in more switches going forward! So kudos to Lofree and Kailh for doing something new.</p><h2 id="plenty-of-gaming-features">Plenty of gaming 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="FomysNB3Q2w3ykmpcHw9hJ" name="Lofree_Hyzen_ 6.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/FomysNB3Q2w3ykmpcHw9hJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>In their magnetic mode, the Nexus switches are great for gaming. You'll find plenty of the zeitgeisty magnetic gaming features we’ve come to expect from today's high end gaming boards — although not quite as many as you'll find on boards like the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air60-he-tiny-profile-huge-performance">NuPhy Air60 HE</a> ($139) and <a href="https://www.tomsguide.com/computing/peripherals/nuphy-field75-he-review">Field75 HE</a> ($169), or the <a href="https://www.tomsguide.com/computing/keyboards/lemokey-p2-he-review-a-tiny-powerhouse">Lemokey P2 HE</a> ($169) and its gamepad analog mode.</p><p>Still, the Hyzen has the big hitters like adjustable actuation, Rapid Trigger, Last Key Prioritization SOCD (A.K.A ‘Snap Tap’), and more. All of this is controlled through the Lofree Web Hub, which I’ll discuss in more detail below.</p><p>I lowered the switch actuation down to around 0.5mm for Counter Strike 2 — quick enough to be extremely responsive without running the risk of accidental inputs. I would normally drop lower, to around 0.3mm, but as I’ll cover later, the Hyzen had significant issues with accidental and repeated inputs at the lowest actuation distances. So I needed a decent (0.3mm) dead zone.</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:1720px;"><p class="vanilla-image-block" style="padding-top:56.28%;"><img id="gjEjb8DAKA9LonPq2rwVjZ" name="Lofree Hyzen Screenshots-2" alt="A screenshot of the Lofree Web Hub being used to customize the Lofree Hyzen" src="https://cdn.mos.cms.futurecdn.net/gjEjb8DAKA9LonPq2rwVjZ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="1720" height="968" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/gjEjb8DAKA9LonPq2rwVjZ-1920-80.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: Lofree / Future)</span></figcaption></figure><p>Still, at 0.5mm, the Nexus switches were super snappy and responsive, and a joy to use in FPS titles like CS2 and Isonzo. For less intense games like Kingdom Come Deliverance 2 and Cities: Skylines 2, I scaled the actuation back to my preferred typing distance of 2.6mm.</p><p>The Hyzen also polls at 8K, for super low latency. In real terms, you won’t notice a difference between a 1K board and an 8K board in terms of latency — 8K is essentially marketing snake oil. But hey, 8K is technically faster than 1K.</p><h2 id="a-mean-productivity-machine">A mean productivity machine</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tfaApM2oLVpDpVey6JyemJ" name="Lofree_Hyzen_ 4.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/tfaApM2oLVpDpVey6JyemJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Hyzen is a serious productivity machine, especially for a 65% board. There’s a neat toggle on the left which turns the number row into Fn buttons, giving you access to productivity shortcuts — which I find especially handy when using macOS at work.</p><p>As I’ll cover later, I had some major input registry issues in mechanical switch mode, but in magnetic mode at 2.6mm actuation and with a hefty amount of dead zone dialled in, I was on <em><strong>fire</strong></em> in the type test. I managed to type at 105WPM with 93% accuracy. That’s 10WPM above my average speed at nearly 5 percentage points above my average accuracy. </p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Words per minute</strong></p></th><th  ><p><strong>Typing accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>Lofree Hyzen</p></td><td  ><p>105</p></td><td  ><p>93%</p></td></tr><tr><td class="firstcol " ><p>Lemokey P2 HE</p></td><td  ><p>100</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/epomaker-he75-mag-review">Epomaker HE75 Mag</a></p></td><td  ><p>99</p></td><td  ><p>92%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/gaming/gaming-peripherals/corsair-vanguard-pro-96-review">Corsair Vanguard Pro 96</a></p></td><td  ><p>105</p></td><td  ><p>96%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/wooting-80he-review">Wooting 80HE</a></p></td><td  ><p>95</p></td><td  ><p>87%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/lemokey-l5-he-review">Lemokey L5 HE</a></p></td><td  ><p>104</p></td><td  ><p>87%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/peripherals/keychron-q16-he-8k-review">Keychron Q16 HE 8K</a></p></td><td  ><p>99</p></td><td  ><p>92%</p></td></tr><tr><td class="firstcol " ><p>NuPhy Air60 HE</p></td><td  ><p>101</p></td><td  ><p>88%</p></td></tr><tr><td class="firstcol " ><p>Keychron K2 HE</p></td><td  ><p>106</p></td><td  ><p>94%</p></td></tr><tr><td class="firstcol " ><p>Pete's rolling averages (all keyboards)</p></td><td  ><p>95.23</p></td><td  ><p>88.34%</p></td></tr></tbody></table></div><p>This is all helped by lovely keycaps. They’re Cherry profile — nothing special there — but finished with a beautiful friction-heavy coating that a) feels great, and b) helps reduce slipping and mistypes.</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="VTJixAk9Ar8FSJwvnELUQJ" name="Lofree_Hyzen_ 7.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/VTJixAk9Ar8FSJwvnELUQJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="caption-text">As you can see, there's a very steep overall angle to the board. </span><span class="credit" itemprop="copyrightHolder">(Image credit: Future)</span></figcaption></figure><p>The keys have a very modest rake, but thanks to a 12-degree rearward pitch angle, the keyboard still feels very steep, and each row is noticeably tiered above the one below. I didn’t get on with this at first, as I’m not accustomed to such a stark difference in the upper- and lower-most key heights. But I got used to it, and it clearly didn’t negatively affect my speed or accuracy! </p><h2 id="captivating-construction">Captivating construction</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="FH8yLdyAztir9JhQk26UFK" name="Lofree_Hyzen_ 1.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/FH8yLdyAztir9JhQk26UFK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Hyzen is utterly gorgeous to behold. Although I expect nothing else from Lofree. The clean, straight lines; the translucent keycaps; the backlight and desklight; the black digital Fn bar. It’s just so slick. Lofree sent me both the gray and white versions, and I’ve had a really hard time choosing which I prefer. Each look fantastic with the RGB backlighting set to a simplistic white, and the desklight set to red. </p><p>Like the <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-2-review">Lofree Flow 2</a> ($149) and <a href="https://www.tomsguide.com/computing/peripherals/lofree-flow84-review-beautiful-but-flawed">Lofree Flow</a> ($159), the Hyzen utilizes a minimalist design ethos. Here, it’s a deliberate attempt to draw focus to the quality of materials used. And they are indeed high quality — thick, machined aluminum alloy for the case; doubleshot PBT keycaps; and an FR4 fiberglass plate. It feels like a premium product.</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="NSzhehZbdzQxJezsUjgMdJ" name="Lofree_Hyzen_ 9.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/NSzhehZbdzQxJezsUjgMdJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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><strong>April 28th 2026: </strong>My units are both early production samples, and a few changes will be made to the appearance of final production units. These include the removal of the (ugly, wonkily-printed) “Hyzen” logo on the space bar; and the addition of macOS legends to keycaps, resulting in dual-labelled caps.</p><h2 id="a-more-ish-muted-clack">A more-ish muted clack</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="2MT2dCdgxiPsNZhMdwnGNK" name="Lofree_Hyzen_ 3.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/2MT2dCdgxiPsNZhMdwnGNK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 Hyzen has a rather more-ish sound. Thanks to the FR4 plate and PBT caps, there’s a muted, plasticky-but-pleasing clack. It isn’t addictive as the poppy <a href="https://www.tomsguide.com/computing/peripherals/wobkey-rainy-75-pro-review">Wobkey Rainy 75 Pro</a> ($139) or the thocky <a href="https://www.tomsguide.com/computing/peripherals/nuphy-halo-75-96-v2-review">NuPhy Halo75 V2</a> ($129), but still rather pleasant.</p><p>Thanks to that heavy aluminum alloy case and a decent amount of internal damping, the Hyzen’s sound is nice and tight, with little in the way of echoes or ricochets. </p><p>Admittedly, my gray sample has an <em>extremely</em> noisy Space bar stabilizer, which generates a metallic ping each time you hit the key. But I’ll cover this more later on.</p><h2 class="article-body__section" id="section-lofree-hyzen-review-the-downs"><span>Lofree Hyzen review: The downs</span></h2><p>Unfortunately, my review units demonstrated poor QC, janky software and switch issues. Then there’s the steep MSRP to consider. The Hyzen is too much of a risk right now for me to recommend.</p><h2 id="poor-quality-control">Poor quality control</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8pP3Fs7JYZMjJnoFMNAwSK" name="Lofree_Hyzen_ 12.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/8pP3Fs7JYZMjJnoFMNAwSK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Unfortunately, my pre-production units suffer from poor quality control. One of my Hyzens likes to rattle — specifically, it has a very noisy, metallic ping from the Space bar stabilizers. It’s a shame because the other unit has a fairly nice sounding Space bar that Lofree has managed to sound-match fairly well with the rest of the keys (within reason, as it’s still a Space bar).</p><p>This isn’t surprising given Lofree’s recent form. Both of my Lofree Flow 2 testers, for example, demonstrated a truly vibe-killing metallic ricochet — a common problem that Lofree has acknowledged to me as QC-related.</p><p>A week or so into testing, and with the battery fully charged, one of my test samples then decided to randomly die... before turning back on 30 minutes later. Plus, the zips on both of my test unit cases were far too small, meaning they caught on the zip fabric and both broke — we had to cut one of the cases open to get to the keyboard (Lofree has confirmed it will revise the zips for production models).</p><h2 id="slightly-janky-software">Slightly janky software</h2><p>With mechanical boards, you can get away with simple open source firmware/software like QMK/VIA. But with magnetic boards, whose core performance relies so much on software customization, the open source platforms just can’t cut it. That’s when you need a solid web app like Keychron’s/Lemokey’s Launcher or NuPhy.io. </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:3200px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="4U4YvCNFqh5Xu5N3J9YwzZ" name="Lofree Hyzen Screenshots-1" alt="A screenshot of the Lofree Web Hub being used to customize the Lofree Hyzen" src="https://cdn.mos.cms.futurecdn.net/4U4YvCNFqh5Xu5N3J9YwzZ-1920-80.jpg" mos="" align="middle" fullscreen="1" width="3200" height="1800" attribution="" endorsement="" class="inline expandable"><a href='https://cdn.mos.cms.futurecdn.net/4U4YvCNFqh5Xu5N3J9YwzZ-1920-80.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: Lofree / Future)</span></figcaption></figure><p>Lofree Hub will be Lofree’s version of those. Right now at least, it works, but it ain’t polished. My early access version was very janky, taking literal minutes to load the background image (like dial up) and buttons, then lagging in use. </p><p>Firmware updating was also pretty clunky, requiring me to download a standalone .EXE (so no updating on Mac) rather than simply updating through the web app, as with Keychron and NuPhy boards.</p><p>Hopefully, the final production software will be an improvement on this. But Lofree’s previous proprietary software platforms have been equally janky, and usually get canned by the manufacturer after one or two boards (see my <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-lite84-review">Lofree Flow Lite review</a>). So, again with Lofree's recent form in mind, forgive me if I’m not full of optimism here.</p><h2 id="dead-zone-required-and-no-mx-bounce-control">Dead zone required and no MX bounce control</h2><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="2MT2dCdgxiPsNZhMdwnGNK" name="Lofree_Hyzen_ 3.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/v2/w:1920,t:215,l:395,cw:960,ch:540,q:80/2MT2dCdgxiPsNZhMdwnGNK.jpg" mos="" align="middle" fullscreen="" width="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 found the Nexus switches in MX mode difficult to use. Despite flawless performance in magnetic mode, almost setting my PBs, I could only just type 90 words per minute  in MX mode at only 87% accuracy. I had repeated/double inputs all over the shop, even after recalibration. MX mode is just crying out for some bounce/debounce settings to iron this out, but the app gives no control over that. I’ve just left the thing in magnetic mode and have reached out to Lofree about the input issues.</p><p>In magnetic mode, there were a lot of accidental inputs at short actuation distances. This is normal (although I’ve never had as many issues as with the Hyzen/Nexus switches), and as I mentioned above, I just dialled in 0.3mm of dead zone to fix the issue at 0.4mm and 0.5mm actuation.</p><p><strong>April 28th 2026: </strong>Lofree has confirmed to me that MX switch issues are known about in pre-production units.</p><h2 id="astronomically-high-price">Astronomically high price</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="tfaApM2oLVpDpVey6JyemJ" name="Lofree_Hyzen_ 4.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/tfaApM2oLVpDpVey6JyemJ-1920-80.jpg" mos="" align="middle" fullscreen="" width="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>Finally, there’s the Hyzen’s high price. There will be some deals for Kickstarter supporters, but the MSRP of the tri-mode variant will be $279. That’s crazy steep — even pricier than high-end magnetic boards like the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q6-he-8k-review"><u>Keychron Q6 8K HE</u></a> ($239) and <a href="https://www.tomsguide.com/computing/peripherals/lemokey-l5-he-review"><u>Lemokey L5 HE</u></a> ($220).</p><p>Despite its promising features, given the number of issues I've experienced, I don't think this board warrants its price. If you're gonna charge that much for a deck, it better be polished. Again, the product is in Kickstarter, so the final production model may have these issues ironed out, but there's no guarantee of that, so I can only go on what I see in front of me.</p><p>Naturally, with Kickstarter there's always the possibility of rug pulls, too. Lofree is an established brand (why use Kickstarter, then?), so I doubt that will happen here, but I'll always advise extreme caution when it comes to crowdfunding projects — that's especially true here given the Hyzen is so clearly unfinished.</p><h2 class="article-body__section" id="section-lofree-hyzen-review-verdict"><span>Lofree Hyzen 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="eBP3vNsqCkLjeMHFw2ppGK" name="Lofree_Hyzen_ 10.JPG" alt="The Lofree Hyzen keyboard on a stone surface" src="https://cdn.mos.cms.futurecdn.net/eBP3vNsqCkLjeMHFw2ppGK-1920-80.jpg" mos="" align="middle" fullscreen="" width="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 love seeing keyboard manufacturers take risks and experiment. Which is why I was so excited about the Hyzen. A dual MX/Mag socket and switch combo has the potential to be the future default setup. In fact, I think it definitely will be. So kudos to Lofree for becoming an early adopter (I’ve no doubt the other big productivity brands will be following suit soon).</p><p>Unfortunately, I can’t recommend the Hyzen in its current form. It’s beautiful to behold, great to type on, sounds pretty good and offers a range of gaming features. But it’s let down by poor QC, plus janky software and switches. I’m really hoping Lofree can iron out these issues before launch. But then oh my lord there’s still <em><strong>that price</strong></em>.</p><p>Lofree: if you’re going to experiment, I’ll back you all the way. But don’t think you can get away with charging <em>double</em> finished-product prices while doing so, for a product that isn’t anywhere near ready for market.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I became a keyboard tinkerer with the Logitech G G512 X, and it seriously upgraded my gaming performance ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’ll be honest. I never got the point of overly-complicated keyboard tinkering. I know all the lingo like Tunnel Magnetoresistance (TMR) analog switches and I know the benefits of this stuff, but I’ve just stuck with my same familiar mechanical keyboard and called it a day. I mean just <em>how </em>much of a difference can all of this tweaking give you?</p><p>Well, as I found out with the Logitech G G512 X, the answer to that is a lot more than I wrongly assumed! I’ve been testing it for a week, and I’ve seen a material difference in my FPS kill/death ratio, reaction speeds and more — all while being a damn good keyboard to type on with a deep 4mm key travel and a satisfyingly tactile thock to each key.</p><p>What grabbed me most about the board is all the built in tinkering potential. The feet double as keycap and switch pullers, nine magnetic analog switches are stored directly on the board itself, and there are second actuation pressure point (SAPP) rings in a handy slot on the underside to add dual functions to a single analog key (like movement speed in an FPS).</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="vNN6XLPtSwH74P4m2wxKkZ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/vNN6XLPtSwH74P4m2wxKkZ-1920-80.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>Over the course of the week I’ve spent testing this, it’s turned me into a maniac of a tinkerer — throw in the RGB customizability you get in G-Hub, and you can tailor the aesthetics and play style to exactly match your needs. And with a healthy chunk of the left-hand side of the board being analog compatible, I tinkered with this for RTS games, indies and more to get <em>just </em>the right upgrade for all genres.</p><p>There are a couple of frustrations, though. There are other similarly-priced boards (like the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review">Keychron K2 HE</a>) that give you analog switches across the entire board, rather than just on a particular portion like the G512 X — leading to a less consistent feel when mixing and matching switches for productivity. On top of that, that SAPP ring storage compartment on the back has a loose grip and you can often find yourself looking under your desk for where one rolled off to.</p><p>But for a keyboard that bridges the gap to analog switches and customization for people like me that just let this whole keyboard revolution pass me by, the G512 X is well-placed to level up the common folk.</p><h2 class="article-body__section" id="section-logitech-g-g512-x-cheat-sheet"><span>Logitech G G512 X: Cheat Sheet</span></h2><ul><li><strong>What is it? </strong>It’s a 75 or 98-key gaming keyboard with customization built into its core.</li><li><strong>Who is it for? </strong>This is ideal for PC gaming enthusiasts and new-starters alike looking to get into the world of analog switch play.</li><li><strong>How much does it cost? </strong>You can pick up the 75-key model for <a href="https://www.logitechg.com/en-us/shop/p/g512-x-75-gaming-keyboard" target="_blank" rel="nofollow">$179</a>/<a href="https://www.logitechg.com/en-gb/shop/p/g512-x-75-gaming-keyboard" target="_blank" rel="nofollow">£169</a>, and the 98-key for <a href="https://www.logitechg.com/en-us/shop/p/g512-x-98-gaming-keyboard" target="_blank" rel="nofollow">$199</a>/<a href="https://www.logitechg.com/en-gb/shop/p/g512-x-98-gaming-keyboard" target="_blank" rel="nofollow">£199</a>. The palm rest you see is an add-on for $39 for the smaller keyboard and $45 for the bigger one.</li><li><strong>What’s good about it? </strong>The customizability built directly into the board makes this impressively versatile, the switch choices made here feel smooth and tactile, and the simplicity of tinkering makes it an addictive pastime.</li><li><strong>Any problems? </strong>A portion of the board is analog-compatible (whereas some of the competition has analog across the board), and the SAPP rings can fall out the back easily.</li></ul><h2 class="article-body__section" id="section-logitech-g-g512-x-specs"><span>Logitech G G512 X: Specs</span></h2><div ><table><thead><tr><th class="firstcol " ><p><strong>Specs</strong></p></th><th  ><p><strong>Logitech G G512 X</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p>$179/169 (75-key) $199/£199 (98-key)</p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>MX Mechanical switches + 9 Gateron KS-20 magnetic analog switches</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p><strong> </strong>PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75 and 98-key layouts</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>True 8Khz report rate</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>13 x 5.9 x 1.9 inches (75-key) 15.2 x 5.9 x 1.9 inches (98-key)</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.9 pounds (75-key) 2.2 pounds (98-key)</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-logitech-g-g512-x-the-ups"><span>Logitech G G512 X: The ups</span></h2><p>And so, the keyboard sprang to life and I got typing. Then I started tinkering, and tinkering some more, and more — to the point that I must admit the G512 X has made me a bit of a keyboard enthusiast.</p><h2 id="stand-out-design-that-fits-into-any-gaming-setup">Stand-out design that fits into any gaming setup</h2><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="br5dpfuomKvLbTvT7AoviJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/br5dpfuomKvLbTvT7AoviJ-1920-80.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>When talking to Logitech, the team spoke about inspirations of modern industrial architecture and a soul rooted in retro design. Born and raised in a working class miner’s town, a lot of this went right over my head, but the end result is a strikingly good-looking keyboard in my opinion.</p><p>The ridged edges give it some textural dynamism (that feels good for persistent fidgeters like me), all your means to customize the board (more on that later) are sensibly built into it, and the RGB emanates a nice surrounding glow. Throw in the use of up to 37% PCR plastics to build it, and you’re onto a winner.</p><p>Then there’s the typing experience itself — height adjustable between 4 and 8 degrees with those removable feet, and those PBT keycaps feel nice on the pads of your fingers with a nice 4mm depth and a tactile landing to make each press feel impactful.</p><p>I did manage to hit a solid 82 words per minute with a 95% accuracy on <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, which is an improvement over my current board for sure! All in all, things are starting strong for productivity and gaming.</p><h2 id="customizability-out-the-wazoo">Customizability out the wazoo</h2><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="3wRFJTHSuusNeWT3ptTt9K" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/3wRFJTHSuusNeWT3ptTt9K-1920-80.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>But this is where things get interesting. If you’re reading this, chances are you’re in one of two camps: you’ve never customized a keyboard before, or you have a box full of spare switches that you have to cart around with you.</p><p>The G512 X is kind of like the Goldilocks keyboard in that way — giving you large levels of customization, but doing it in <em>just </em>the right way that noobies can get on board with. You have keycap and switch pullers that double as keyboard feet, nine TMR analog switches are packed into their own enclosure on the top edge, and your SAPP rings are conveniently stashed in a slot on the underside.</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="z9UBYDA7gX326Apy8imeaJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/z9UBYDA7gX326Apy8imeaJ-1920-80.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>Put simply, it puts everything in one place for better organization of your tools, and I rather love that. The 39 analog switchable keys are clearly identified with a striped pattern across the bottom deck, so you’re never lost on where to put them, and the SAPP rings are a cinch to just stick inside the keys for two-step pressure sensitivity. Oh, and there’s a scan button on the back so you can clearly identify which keys have analog switches!</p><p>Throw in the G-Hub software giving you a range of preset RGB lighting sequences (shoutout to the 2001: A Space Odyssey reference for one of them), or full custom per-key lighting that you can paint onto your keyboard with a nice UI, and I promise you’ll catch that tinkering bug fast.</p><h2 id="it-actually-improved-my-gaming-performance">It actually improved my gaming 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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wAhmHUvhimSGdCY6CZqhhZ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/wAhmHUvhimSGdCY6CZqhhZ-1920-80.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>Of course, it can type well and look nice, but when it all comes together, I ended up with a board I felt made more competitive in online FPS multiplayer than usual! Normally, I’m absolute trash on Call of Duty — not necessarily thinking about the most effective ways I can use my board to be in sync with my play style.</p><p>With those TMR switches and SAPP rings, I was able to map run and sprint to two pressure points of the W key, and those high resolution sensors with a true 8KHz polling rate (as in throughout every part of a key registration) meant my responses were faster and play felt more intuitive for me to focus on navigating while moving.</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="mv6YnQz7jjn9RXLxk2xotJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/mv6YnQz7jjn9RXLxk2xotJ-1920-80.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>Add rapid trigger response for quicker strafes, per key actuation tweaking that allowed me to set the A and D keys to be feather light for said strafes, and key priority to prevent conflicting inputs, and I became quite a beast in the levelled lobbies I played in.</p><p>Did I go back to being terrible when I went up to a higher skill level lobby? Absolutely. I’m 35 — might as well have one foot in the grave compared to these cracked players. But this keyboard demonstrates that the technology can bring out the ability, and now I need to grind.</p><h2 class="article-body__section" id="section-logitech-g-g512-x-the-downs"><span>Logitech G G512 X: The downs</span></h2><p>But while we’re off to a great start, not all is kosher with the G512 X. There’s a couple of frustrations that I had with the board while using it.</p><h2 id="the-runaway-sapp-rings">The runaway SAPP rings</h2><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="JRSRdqkkAzKjFyDQFhsiwJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/JRSRdqkkAzKjFyDQFhsiwJ-1920-80.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>So on one hand, I applaud Logitech for going about keyboard modding in a fun, interesting way. By the way, those switch holes are compatible with most other switches you may buy aftermarket.</p><p>But that SAPP ring holster is not the best at…well…holstering the SAPP rings. Small movements or tilts can cause one or two to fall out, and you may be left crouching down and looking for these translucent rings hiding in the fibers of your carpet.</p><p>Provided you’re gentle moving it around, you’ll be fine. But would it kill logitech to add a door to this just like the container for the switches?</p><h2 id="not-an-entirely-analog-keyboard">Not an entirely analog keyboard</h2><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="7HB3zzCh7kQWz39C4SRLEJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/7HB3zzCh7kQWz39C4SRLEJ-1920-80.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>39 of the keys are analog TMR switch compatible on the G512 X, but on other keyboards like the Keychron K2 HE, you’ll find the entire board packs analog. For most play styles, not the worst thing in the world — Logitech has specifically picked the keys you’ll be using mostly with your left hand for navigating.</p><p>But that does mean lefties may struggle slightly, as the entire right half cannot be analog-ed.</p><h2 class="article-body__section" id="section-logitech-g-g512-x-verdict"><span>Logitech G G512 X: Verdict</span></h2><p>So I would recommend you think of this as a bridging gap for those who are ready to make the jump to the enthusiast level. If you’re pro-tier, there are other pro boards that are more worthwhile here. But for me specifically — the guy who is new to tinkering, this is a gateway drug to that.</p><p>The G512 X looks slick in my setup, has a really nice typing experience, and has genuinely helped me improve in gaming with its analog switches.</p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/logitech-g-g512-x-review</link>
                                                                            <description>
                            <![CDATA[ The Logitech G G512 X is a customizable analog gaming keyboard for the rest of us with a great, tactile key press feel and a bold, vibrant design. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">6CHRTdT3juZVAFyan9pGSN</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/TZzzCCScbbhnvWARC2RfEL-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Tue, 28 Apr 2026 07:00:00 +0000</pubDate>                                                                                                                                <updated>Tue, 28 Apr 2026 07:33:54 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></category>
                                                                                                                    <dc:creator><![CDATA[ Jason England ]]></dc:creator>                                                                                    <dc:source><![CDATA[ https://cdn.mos.cms.futurecdn.net/v4fSq5U4uZUEtGY2BwNuJ6-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Jason brings a decade of tech and gaming journalism experience to his role as a Managing Editor of Computing at Tom&#039;s Guide. He has previously written for Laptop Mag, Tom&#039;s Hardware, Kotaku, Stuff and BBC Science Focus. In his spare time, you&#039;ll find Jason looking for good dogs to pet or thinking about eating pizza if he isn&#039;t already.&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/TZzzCCScbbhnvWARC2RfEL-1920-80.jpg">
                                                            <media:credit><![CDATA[Future]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[Logitech G G512 X]]></media:description>                                                            <media:text><![CDATA[Logitech G G512 X]]></media:text>
                                <media:title type="plain"><![CDATA[Logitech G G512 X]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/TZzzCCScbbhnvWARC2RfEL-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’ll be honest. I never got the point of overly-complicated keyboard tinkering. I know all the lingo like Tunnel Magnetoresistance (TMR) analog switches and I know the benefits of this stuff, but I’ve just stuck with my same familiar mechanical keyboard and called it a day. I mean just <em>how </em>much of a difference can all of this tweaking give you?</p><p>Well, as I found out with the Logitech G G512 X, the answer to that is a lot more than I wrongly assumed! I’ve been testing it for a week, and I’ve seen a material difference in my FPS kill/death ratio, reaction speeds and more — all while being a damn good keyboard to type on with a deep 4mm key travel and a satisfyingly tactile thock to each key.</p><p>What grabbed me most about the board is all the built in tinkering potential. The feet double as keycap and switch pullers, nine magnetic analog switches are stored directly on the board itself, and there are second actuation pressure point (SAPP) rings in a handy slot on the underside to add dual functions to a single analog key (like movement speed in an FPS).</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="vNN6XLPtSwH74P4m2wxKkZ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/vNN6XLPtSwH74P4m2wxKkZ-1920-80.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>Over the course of the week I’ve spent testing this, it’s turned me into a maniac of a tinkerer — throw in the RGB customizability you get in G-Hub, and you can tailor the aesthetics and play style to exactly match your needs. And with a healthy chunk of the left-hand side of the board being analog compatible, I tinkered with this for RTS games, indies and more to get <em>just </em>the right upgrade for all genres.</p><p>There are a couple of frustrations, though. There are other similarly-priced boards (like the <a href="https://www.tomsguide.com/computing/peripherals/keychron-k2-he-review">Keychron K2 HE</a>) that give you analog switches across the entire board, rather than just on a particular portion like the G512 X — leading to a less consistent feel when mixing and matching switches for productivity. On top of that, that SAPP ring storage compartment on the back has a loose grip and you can often find yourself looking under your desk for where one rolled off to.</p><p>But for a keyboard that bridges the gap to analog switches and customization for people like me that just let this whole keyboard revolution pass me by, the G512 X is well-placed to level up the common folk.</p><h2 class="article-body__section" id="section-logitech-g-g512-x-cheat-sheet"><span>Logitech G G512 X: Cheat Sheet</span></h2><ul><li><strong>What is it? </strong>It’s a 75 or 98-key gaming keyboard with customization built into its core.</li><li><strong>Who is it for? </strong>This is ideal for PC gaming enthusiasts and new-starters alike looking to get into the world of analog switch play.</li><li><strong>How much does it cost? </strong>You can pick up the 75-key model for <a href="https://www.logitechg.com/en-us/shop/p/g512-x-75-gaming-keyboard" target="_blank" rel="nofollow">$179</a>/<a href="https://www.logitechg.com/en-gb/shop/p/g512-x-75-gaming-keyboard" target="_blank" rel="nofollow">£169</a>, and the 98-key for <a href="https://www.logitechg.com/en-us/shop/p/g512-x-98-gaming-keyboard" target="_blank" rel="nofollow">$199</a>/<a href="https://www.logitechg.com/en-gb/shop/p/g512-x-98-gaming-keyboard" target="_blank" rel="nofollow">£199</a>. The palm rest you see is an add-on for $39 for the smaller keyboard and $45 for the bigger one.</li><li><strong>What’s good about it? </strong>The customizability built directly into the board makes this impressively versatile, the switch choices made here feel smooth and tactile, and the simplicity of tinkering makes it an addictive pastime.</li><li><strong>Any problems? </strong>A portion of the board is analog-compatible (whereas some of the competition has analog across the board), and the SAPP rings can fall out the back easily.</li></ul><h2 class="article-body__section" id="section-logitech-g-g512-x-specs"><span>Logitech G G512 X: Specs</span></h2><div ><table><thead><tr><th class="firstcol " ><p><strong>Specs</strong></p></th><th  ><p><strong>Logitech G G512 X</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p>$179/169 (75-key) $199/£199 (98-key)</p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>MX Mechanical switches + 9 Gateron KS-20 magnetic analog switches</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p><strong> </strong>PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Gasket</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75 and 98-key layouts</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>True 8Khz report rate</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Wired</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>13 x 5.9 x 1.9 inches (75-key) 15.2 x 5.9 x 1.9 inches (98-key)</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>1.9 pounds (75-key) 2.2 pounds (98-key)</p></td></tr></tbody></table></div><h2 class="article-body__section" id="section-logitech-g-g512-x-the-ups"><span>Logitech G G512 X: The ups</span></h2><p>And so, the keyboard sprang to life and I got typing. Then I started tinkering, and tinkering some more, and more — to the point that I must admit the G512 X has made me a bit of a keyboard enthusiast.</p><h2 id="stand-out-design-that-fits-into-any-gaming-setup">Stand-out design that fits into any gaming setup</h2><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="br5dpfuomKvLbTvT7AoviJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/br5dpfuomKvLbTvT7AoviJ-1920-80.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>When talking to Logitech, the team spoke about inspirations of modern industrial architecture and a soul rooted in retro design. Born and raised in a working class miner’s town, a lot of this went right over my head, but the end result is a strikingly good-looking keyboard in my opinion.</p><p>The ridged edges give it some textural dynamism (that feels good for persistent fidgeters like me), all your means to customize the board (more on that later) are sensibly built into it, and the RGB emanates a nice surrounding glow. Throw in the use of up to 37% PCR plastics to build it, and you’re onto a winner.</p><p>Then there’s the typing experience itself — height adjustable between 4 and 8 degrees with those removable feet, and those PBT keycaps feel nice on the pads of your fingers with a nice 4mm depth and a tactile landing to make each press feel impactful.</p><p>I did manage to hit a solid 82 words per minute with a 95% accuracy on <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>, which is an improvement over my current board for sure! All in all, things are starting strong for productivity and gaming.</p><h2 id="customizability-out-the-wazoo">Customizability out the wazoo</h2><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="3wRFJTHSuusNeWT3ptTt9K" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/3wRFJTHSuusNeWT3ptTt9K-1920-80.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>But this is where things get interesting. If you’re reading this, chances are you’re in one of two camps: you’ve never customized a keyboard before, or you have a box full of spare switches that you have to cart around with you.</p><p>The G512 X is kind of like the Goldilocks keyboard in that way — giving you large levels of customization, but doing it in <em>just </em>the right way that noobies can get on board with. You have keycap and switch pullers that double as keyboard feet, nine TMR analog switches are packed into their own enclosure on the top edge, and your SAPP rings are conveniently stashed in a slot on the underside.</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="z9UBYDA7gX326Apy8imeaJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/z9UBYDA7gX326Apy8imeaJ-1920-80.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>Put simply, it puts everything in one place for better organization of your tools, and I rather love that. The 39 analog switchable keys are clearly identified with a striped pattern across the bottom deck, so you’re never lost on where to put them, and the SAPP rings are a cinch to just stick inside the keys for two-step pressure sensitivity. Oh, and there’s a scan button on the back so you can clearly identify which keys have analog switches!</p><p>Throw in the G-Hub software giving you a range of preset RGB lighting sequences (shoutout to the 2001: A Space Odyssey reference for one of them), or full custom per-key lighting that you can paint onto your keyboard with a nice UI, and I promise you’ll catch that tinkering bug fast.</p><h2 id="it-actually-improved-my-gaming-performance">It actually improved my gaming 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:3840px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="wAhmHUvhimSGdCY6CZqhhZ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/wAhmHUvhimSGdCY6CZqhhZ-1920-80.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>Of course, it can type well and look nice, but when it all comes together, I ended up with a board I felt made more competitive in online FPS multiplayer than usual! Normally, I’m absolute trash on Call of Duty — not necessarily thinking about the most effective ways I can use my board to be in sync with my play style.</p><p>With those TMR switches and SAPP rings, I was able to map run and sprint to two pressure points of the W key, and those high resolution sensors with a true 8KHz polling rate (as in throughout every part of a key registration) meant my responses were faster and play felt more intuitive for me to focus on navigating while moving.</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="mv6YnQz7jjn9RXLxk2xotJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/mv6YnQz7jjn9RXLxk2xotJ-1920-80.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>Add rapid trigger response for quicker strafes, per key actuation tweaking that allowed me to set the A and D keys to be feather light for said strafes, and key priority to prevent conflicting inputs, and I became quite a beast in the levelled lobbies I played in.</p><p>Did I go back to being terrible when I went up to a higher skill level lobby? Absolutely. I’m 35 — might as well have one foot in the grave compared to these cracked players. But this keyboard demonstrates that the technology can bring out the ability, and now I need to grind.</p><h2 class="article-body__section" id="section-logitech-g-g512-x-the-downs"><span>Logitech G G512 X: The downs</span></h2><p>But while we’re off to a great start, not all is kosher with the G512 X. There’s a couple of frustrations that I had with the board while using it.</p><h2 id="the-runaway-sapp-rings">The runaway SAPP rings</h2><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="JRSRdqkkAzKjFyDQFhsiwJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/JRSRdqkkAzKjFyDQFhsiwJ-1920-80.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>So on one hand, I applaud Logitech for going about keyboard modding in a fun, interesting way. By the way, those switch holes are compatible with most other switches you may buy aftermarket.</p><p>But that SAPP ring holster is not the best at…well…holstering the SAPP rings. Small movements or tilts can cause one or two to fall out, and you may be left crouching down and looking for these translucent rings hiding in the fibers of your carpet.</p><p>Provided you’re gentle moving it around, you’ll be fine. But would it kill logitech to add a door to this just like the container for the switches?</p><h2 id="not-an-entirely-analog-keyboard">Not an entirely analog keyboard</h2><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="7HB3zzCh7kQWz39C4SRLEJ" name="Logitech G G512 X" alt="Logitech G G512 X" src="https://cdn.mos.cms.futurecdn.net/7HB3zzCh7kQWz39C4SRLEJ-1920-80.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>39 of the keys are analog TMR switch compatible on the G512 X, but on other keyboards like the Keychron K2 HE, you’ll find the entire board packs analog. For most play styles, not the worst thing in the world — Logitech has specifically picked the keys you’ll be using mostly with your left hand for navigating.</p><p>But that does mean lefties may struggle slightly, as the entire right half cannot be analog-ed.</p><h2 class="article-body__section" id="section-logitech-g-g512-x-verdict"><span>Logitech G G512 X: Verdict</span></h2><p>So I would recommend you think of this as a bridging gap for those who are ready to make the jump to the enthusiast level. If you’re pro-tier, there are other pro boards that are more worthwhile here. But for me specifically — the guy who is new to tinkering, this is a gateway drug to that.</p><p>The G512 X looks slick in my setup, has a really nice typing experience, and has genuinely helped me improve in gaming with its analog switches.</p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ The most impressive mechanical keyboard in the room is the one you barely notice — and the Iqunix Magi75 proves it ]]></title>
                                                                                                <dc:content><![CDATA[ <p>Mechanical keyboards can get loud. <em>Really loud. </em>Loud to the point that your coworkers  ask you to turn it down. Mechanical keyboards can be really quiet too, to the point that your coworkers start wondering if you're actually working or simply slacking off.</p><p>In addition, Mech decks can also look really obnoxious. You know what I'm talking about. Flashy, loud, garish RGB lighting shining through keycaps, enough to light up your entire desk and encroach on your coworker's space too. I'm not a fan of loud keyboards, and personally prefer a minimalist one. Maximalism is not my style, and recently, I tested a keyboard that proved to me that low-profile is the new high-end — and I mean that both in terms of the keyboard's <em>actual </em>profile and conspicuity.</p><div class="product"><a data-dimension112="83f3941a-cf4f-4b8f-a7fd-1b638f45bebd" data-action="Deal Block" data-label="Magi75" data-dimension48="Magi75" data-dimension25="$149" href="https://www.amazon.com/IQUNIX-Wireless-Mechanical-Keyboard-Mouse/dp/B0FK4WM767/" 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:57.33%;"><img id="eWmj6ffc7K76s3C6XVdvjg" name="Magi75-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/eWmj6ffc7K76s3C6XVdvjg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="860" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Iqunix Magi75 is a capable and competent mechanical keyboard, and its low-profile design offers a comfortable typing experience. Its Gold Red linear switches are an utter delight to type on, and listen to. The Magi75’s styling is gorgeous, and the build quality is top-notch.<a class="view-deal button" href="https://www.amazon.com/IQUNIX-Wireless-Mechanical-Keyboard-Mouse/dp/B0FK4WM767/" target="_blank" rel="nofollow" data-dimension112="83f3941a-cf4f-4b8f-a7fd-1b638f45bebd" data-action="Deal Block" data-label="Magi75" data-dimension48="Magi75" data-dimension25="$149">View Deal</a></p></div><p>The <a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a> is a beautiful keyboard. It looks oh-so-good, and its low-profile design has some great benefits for those who have sedentary jobs, where they sit at a desk for eight or more hours a day (like me!). Its Kailh Gold Red switches are simply drool-worthy, for typing on and listening to. Did I mention this is a stunning board? I'll say it a few more times in any case. Here's why I love the Magi 75.</p><h2 id="mmmm-kailh-switches">Mmmm, Kailh switches</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8n9zN9HxpoYV2NohEGUWTd" name="IQUNIX_MG75_Mechanical_keyboard_009.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/8n9zN9HxpoYV2NohEGUWTd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards</a> in my time as a reviews writer (over 80, last I counted), and I've always been a bit partial to mech decks. I love the tactile bump of a mechanical switch — something Hall Effect ones simply can't match, in my opinion. The Iqunix Magi75 features some of the best and smoothest switches I've ever used, and they're the Kailh Gold Red linears.</p><p>The Kailh Gold Reds require just 40g of force to activate, and they have a pre-travel distance of 1.2mm (2.8mm before bottoming out) so you don’t even need to press them all the way down to activate them. This makes your fingers feel like they're bouncing on clouds. The typing feel is nothing short of beautiful — and fast.</p><p>Alongside that, the Gold Reds work with the Magi75's le-tray mount design and several layers of sound dampening foam to deliver a creamy sound profile. It's not too thocky or clicky that it fills your coworkers with rage, and it isn't too quiet that you risk coming across as someone who isn't working at all. Now, I absolutely love this. I had two colleagues point out that they really loved the sound, and both of them complimented the keyboard's design too, which brings me to my next point...</p><h2 id="maximalism-is-out-minimalism-is-in">Maximalism is out, minimalism is in</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HHHz2YAiPSayLyK5fACuVc" name="IQUNIX_MG75_Mechanical_keyboard_006.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/HHHz2YAiPSayLyK5fACuVc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 love the Iqunix Magi75's low-key design. You can get the keyboard in either black or white, with either pinkish-red or gold accents gracing the keycaps. Customizable per-key RGB lighting is at play too, but it isn't too loud or bright even at its highest brightness. Another thing I love about the Magi75 is its aluminum case, which lends the keyboard a premium, top-notch build and look.</p><p>In my opinion, and as I alluded to in the headline, the most impressive mechanical keyboard in the room is the one you barely notice. The Magi75 is just that. While maintaining a sleek and inconspicuous design, it enables you to type quickly and get the job done.</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="npWLVbdyzgEMA9EbFTLEGd" name="IQUNIX_MG75_Mechanical_keyboard_011.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/npWLVbdyzgEMA9EbFTLEGd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>That's also thanks to its low-profile design which has health and ergonomic benefits. Since the keyboard sits flush against your desk's surface, your hands fall naturally on it, and you don't need an additional wrist rest to feel comfortable, as you do with standard-profile keyboards. This ergonomic design means you can type all day long without your fingers feeling fatigued. Low-profile decks can help reduce the risk of repetitive strain injuries (RSI), especially if you have a sedentary job.</p><p>In addition to this, low-profile keyboards use low-profile switches and keycaps. LP switches don't require as much force to activate, and they have shorter travel distances compared to their standard-profile counterparts. This means that you can type quickly and, again, your fingers don't feel tired. I honestly don't know how anyone could continue using a standard-profile keyboard for <em>all-day long work. </em>The benefits of low-profile decks easily outweigh any shortcomings they may have (and the Magi75 does).</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OLQLbX"></div>                            </div>                            <script src="https://kwizly.com/embed/OLQLbX.js" async></script><h2 id="so-so-close-to-greatness">So, so close to greatness</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="prJxnuMAyP2v4LYvFCzVkc" name="IQUNIX_MG75_Mechanical_keyboard_016.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/prJxnuMAyP2v4LYvFCzVkc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 you can probably tell by now, I really love the Iqunix Magi75, which is why I wish it didn't have the flaws it does. The Magi75, although great for productivity, has noticeable drawbacks. While the Kailh Gold Red linear switches are phenomenal, Iqunix doesn't give you any other switches to choose from — instead, you must buy the Kailh 1353 series separately.</p><p>There's also no physical toggles to turn on the keyboard or swap between Bluetooth and 2.4GHz connections, which means you must rely on key combinations. That's fine, and I'm aware it's difficult to fit physical toggles on aluminum cases, but I found the key combinations to be a little finicky in my testing, with my combos not registering.</p><p>Last but not least, the Magi75's height can't be adjusted, which can be frustrating for users who want to prop the keyboard up at an angle. But are <em>all </em>of these dealbreakers? Not necessarily. As I noted in my full <a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75 review</a>, if it's a well-rounded, fast typing feel and lovely sound you're after, this keyboard more than suffices, and the low-profile, minimalist design is just too good. Outstanding. Beautiful. Fantastic. Take your pick!</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/best-picks/best-mechanical-keyboards">The best mechanical keyboards in 2026: We reviewed 100+ models to find the best 'thock'</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">I thought I’d fall in love with the Iqunix Magi75 — but this low-profile mechanical keyboard didn’t pass the vibe check</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li></ul><div data-widget-type="review" data-model-name="Iqunix Magi75"></div> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/the-most-impressive-mechanical-keyboard-in-the-room-is-the-one-you-barely-notice-and-the-iqunix-magi75-proves-it</link>
                                                                            <description>
                            <![CDATA[ Low-profile is the new high-end, and the Iqunix Magi75 offers one of the best typing experiences packed into a stunning, minimalist body. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">9yYhCzv6FChQaht8wLzN3j</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/9z9dpRFQVRJ3RydEUtPfzd-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Sun, 26 Apr 2026 08:30:00 +0000</pubDate>                                                                                                                                <updated>Mon, 27 Apr 2026 03:44:21 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/9z9dpRFQVRJ3RydEUtPfzd-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Iqunix Magi75 mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A black Iqunix Magi75 mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A black Iqunix Magi75 mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/9z9dpRFQVRJ3RydEUtPfzd-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>Mechanical keyboards can get loud. <em>Really loud. </em>Loud to the point that your coworkers  ask you to turn it down. Mechanical keyboards can be really quiet too, to the point that your coworkers start wondering if you're actually working or simply slacking off.</p><p>In addition, Mech decks can also look really obnoxious. You know what I'm talking about. Flashy, loud, garish RGB lighting shining through keycaps, enough to light up your entire desk and encroach on your coworker's space too. I'm not a fan of loud keyboards, and personally prefer a minimalist one. Maximalism is not my style, and recently, I tested a keyboard that proved to me that low-profile is the new high-end — and I mean that both in terms of the keyboard's <em>actual </em>profile and conspicuity.</p><div class="product"><a data-dimension112="83f3941a-cf4f-4b8f-a7fd-1b638f45bebd" data-action="Deal Block" data-label="Magi75" data-dimension48="Magi75" data-dimension25="$149" href="https://www.amazon.com/IQUNIX-Wireless-Mechanical-Keyboard-Mouse/dp/B0FK4WM767/" 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:57.33%;"><img id="eWmj6ffc7K76s3C6XVdvjg" name="Magi75-deal" caption="" alt="" src="https://cdn.mos.cms.futurecdn.net/eWmj6ffc7K76s3C6XVdvjg-1920-80.jpg" mos="" align="middle" fullscreen="" width="1500" height="860" attribution="" endorsement="" credit="" class=""></p></div></div></figure></a><p>The Iqunix Magi75 is a capable and competent mechanical keyboard, and its low-profile design offers a comfortable typing experience. Its Gold Red linear switches are an utter delight to type on, and listen to. The Magi75’s styling is gorgeous, and the build quality is top-notch.<a class="view-deal button" href="https://www.amazon.com/IQUNIX-Wireless-Mechanical-Keyboard-Mouse/dp/B0FK4WM767/" target="_blank" rel="nofollow" data-dimension112="83f3941a-cf4f-4b8f-a7fd-1b638f45bebd" data-action="Deal Block" data-label="Magi75" data-dimension48="Magi75" data-dimension25="$149">View Deal</a></p></div><p>The <a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75</a> is a beautiful keyboard. It looks oh-so-good, and its low-profile design has some great benefits for those who have sedentary jobs, where they sit at a desk for eight or more hours a day (like me!). Its Kailh Gold Red switches are simply drool-worthy, for typing on and listening to. Did I mention this is a stunning board? I'll say it a few more times in any case. Here's why I love the Magi 75.</p><h2 id="mmmm-kailh-switches">Mmmm, Kailh switches</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8n9zN9HxpoYV2NohEGUWTd" name="IQUNIX_MG75_Mechanical_keyboard_009.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/8n9zN9HxpoYV2NohEGUWTd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 the <a href="https://www.tomsguide.com/best-picks/best-keyboard">best keyboards</a> in my time as a reviews writer (over 80, last I counted), and I've always been a bit partial to mech decks. I love the tactile bump of a mechanical switch — something Hall Effect ones simply can't match, in my opinion. The Iqunix Magi75 features some of the best and smoothest switches I've ever used, and they're the Kailh Gold Red linears.</p><p>The Kailh Gold Reds require just 40g of force to activate, and they have a pre-travel distance of 1.2mm (2.8mm before bottoming out) so you don’t even need to press them all the way down to activate them. This makes your fingers feel like they're bouncing on clouds. The typing feel is nothing short of beautiful — and fast.</p><p>Alongside that, the Gold Reds work with the Magi75's le-tray mount design and several layers of sound dampening foam to deliver a creamy sound profile. It's not too thocky or clicky that it fills your coworkers with rage, and it isn't too quiet that you risk coming across as someone who isn't working at all. Now, I absolutely love this. I had two colleagues point out that they really loved the sound, and both of them complimented the keyboard's design too, which brings me to my next point...</p><h2 id="maximalism-is-out-minimalism-is-in">Maximalism is out, minimalism is in</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="HHHz2YAiPSayLyK5fACuVc" name="IQUNIX_MG75_Mechanical_keyboard_006.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/HHHz2YAiPSayLyK5fACuVc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 love the Iqunix Magi75's low-key design. You can get the keyboard in either black or white, with either pinkish-red or gold accents gracing the keycaps. Customizable per-key RGB lighting is at play too, but it isn't too loud or bright even at its highest brightness. Another thing I love about the Magi75 is its aluminum case, which lends the keyboard a premium, top-notch build and look.</p><p>In my opinion, and as I alluded to in the headline, the most impressive mechanical keyboard in the room is the one you barely notice. The Magi75 is just that. While maintaining a sleek and inconspicuous design, it enables you to type quickly and get the job done.</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="npWLVbdyzgEMA9EbFTLEGd" name="IQUNIX_MG75_Mechanical_keyboard_011.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/npWLVbdyzgEMA9EbFTLEGd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>That's also thanks to its low-profile design which has health and ergonomic benefits. Since the keyboard sits flush against your desk's surface, your hands fall naturally on it, and you don't need an additional wrist rest to feel comfortable, as you do with standard-profile keyboards. This ergonomic design means you can type all day long without your fingers feeling fatigued. Low-profile decks can help reduce the risk of repetitive strain injuries (RSI), especially if you have a sedentary job.</p><p>In addition to this, low-profile keyboards use low-profile switches and keycaps. LP switches don't require as much force to activate, and they have shorter travel distances compared to their standard-profile counterparts. This means that you can type quickly and, again, your fingers don't feel tired. I honestly don't know how anyone could continue using a standard-profile keyboard for <em>all-day long work. </em>The benefits of low-profile decks easily outweigh any shortcomings they may have (and the Magi75 does).</p><div style="min-height: 250px;">                                <div class="kwizly-quiz kwizly-OLQLbX"></div>                            </div>                            <script src="https://kwizly.com/embed/OLQLbX.js" async></script><h2 id="so-so-close-to-greatness">So, so close to greatness</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="prJxnuMAyP2v4LYvFCzVkc" name="IQUNIX_MG75_Mechanical_keyboard_016.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/prJxnuMAyP2v4LYvFCzVkc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 you can probably tell by now, I really love the Iqunix Magi75, which is why I wish it didn't have the flaws it does. The Magi75, although great for productivity, has noticeable drawbacks. While the Kailh Gold Red linear switches are phenomenal, Iqunix doesn't give you any other switches to choose from — instead, you must buy the Kailh 1353 series separately.</p><p>There's also no physical toggles to turn on the keyboard or swap between Bluetooth and 2.4GHz connections, which means you must rely on key combinations. That's fine, and I'm aware it's difficult to fit physical toggles on aluminum cases, but I found the key combinations to be a little finicky in my testing, with my combos not registering.</p><p>Last but not least, the Magi75's height can't be adjusted, which can be frustrating for users who want to prop the keyboard up at an angle. But are <em>all </em>of these dealbreakers? Not necessarily. As I noted in my full <a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">Iqunix Magi75 review</a>, if it's a well-rounded, fast typing feel and lovely sound you're after, this keyboard more than suffices, and the low-profile, minimalist design is just too good. Outstanding. Beautiful. Fantastic. Take your pick!</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/best-picks/best-mechanical-keyboards">The best mechanical keyboards in 2026: We reviewed 100+ models to find the best 'thock'</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review">I thought I’d fall in love with the Iqunix Magi75 — but this low-profile mechanical keyboard didn’t pass the vibe check</a></li><li><a href="https://www.tomsguide.com/computing/keyboards/im-sick-of-pretending-hall-effect-keyboards-are-the-end-all-be-all-heres-why-id-still-pick-a-mechanical-deck-in-2026">I’m sick of pretending Hall Effect keyboards are the be-all end-all — here’s why I’d still pick a mechanical deck in 2026</a></li></ul><div data-widget-type="review" data-model-name="Iqunix Magi75"></div>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
                                <item>
                                                            <title><![CDATA[ I thought I’d fall in love with the IQUNIX Magi75 — but this low-profile mechanical keyboard didn’t pass the vibe check ]]></title>
                                                                                                <dc:content><![CDATA[ <p>I’ve tested a lot of keyboards as a reviews writer. Last I counted, I’d reviewed over 80 keyboards — magnetic and mechanical. The most recent deck I’ve tested is the IQUNIX Magi75, a mechanical keyboard with beautiful Kailh Gold Red switches which are a delight to type on. The keyboard sounds lovely with a creamy profile, and it looks great, sporting a minimalist design and an aluminum build. Customization is super easy thanks to web-based software, and the Magi75 is suited to casual gaming too.</p><p>But the Magi75 isn’t perfect, and given how much fun the typing experience is, I was disappointed by its flaws. Very limited stock switch choice and the non-adjustable height are considerable drawbacks, as are the confusing key combos for changing settings and the premium price.</p><p>To find out if it’s one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> for you, read my full IQUNIX Magi75 review.</p><h2 class="article-body__section" id="section-iqunix-magi75-review-specs"><span>IQUNIX Magi75 review: Specs</span></h2><div ><table><thead><tr><th class="firstcol " ><p><strong>Specs</strong></p></th><th  ><p><strong>IQUNIX Magi75</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/IQUNIX-Magi75-Mechanical-Hot-Swappable-Customizable/dp/B0FK4WM767/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/Mechanical-Keyboard-Tri-Mode-Hot-Swappable-Customizable/dp/B0FL2YVN4C/" target="_blank" rel="nofollow">£195</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Kailh Gold Red (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Le-tray</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 84 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz); 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>4,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated, RGB off)</strong></p></td><td  ><p>500 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated, RGB on)</strong></p></td><td  ><p>10 hours (all lighting); 10.5 hours (backlight only)</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.8 x 5.3 x 1.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.08lbs</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-iqunix-magi75-review-cheat-sheet"><span>IQUNIX Magi75 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A low-profile mechanical keyboard with a 75% layout</li><li><strong>Who is it for? </strong>Hardcore typists who want a thoroughly responsive (and musically sound) typing experience</li><li><strong>How much does it cost? </strong>The IQUNIX Magi75 is available for <a href="https://www.amazon.com/IQUNIX-Magi75-Mechanical-Hot-Swappable-Customizable/dp/B0FK4WM767/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/Mechanical-Keyboard-Tri-Mode-Hot-Swappable-Customizable/dp/B0FL2YVN4C/" target="_blank" rel="nofollow">£195</a></li><li><strong>What do we like? </strong>The high quality build, lovely design, comfortable low-profile design, typing experience, and user-friendly companion software</li><li><strong>What don’t we like? </strong>The confusing key combos for settings, non-adjustable height, limited stock switch choice, and premium price</li></ul><h2 class="article-body__section" id="section-iqunix-magi75-review-the-ups"><span>IQUNIX Magi75 review: The ups</span></h2><p>From its exceptional build quality and lovely typing feel and sound, to its user-friendly web-based software, the IQUNIX Magi75 is a winner.</p><h2 id="built-like-a-gorgeous-tank">Built like a (gorgeous) tank</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TpStcnAqUpErD9evzsP8Kc" name="IQUNIX_MG75_Mechanical_keyboard_004.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/TpStcnAqUpErD9evzsP8Kc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 IQUNIX Magi75 is one of the best built keyboards I’ve had the pleasure of using or testing. Utilizing a combination of plastic and CNC machined aluminum, this thing is built like a tank. The aluminum case feels premium and high-quality, and is scratch-resistant, so you can rest assured that it won’t get too damaged over time.</p><p>Because it features an aluminum case and is a low-profile keyboard, the Magi75’s closest competitors are the <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-2-review">Lofree Flow 2</a> ($149), the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139) and the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129). The first two feature all-aluminum bottom cases while the Hexgears uses a combination of aluminum and plastic.</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="A9qK4ukXD7Ej35QRaZ9VUd" name="IQUNIX_MG75_Mechanical_keyboard_007.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/A9qK4ukXD7Ej35QRaZ9VUd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Also aiding the Magi75’s structural integrity is the le-tray mount design, which has been developed by IQUNIX and features four PCB support arms and silicone rings, as well as several layers of sound-dampening foam. The le-tray mount design, according to IQUNIX, “provides more bottom-out space within the compact low-profile keyboard layout.” In turn, it delivers a soft typing feel and a creamy sound profile. All of this also contributes to a comfortable and non-fatiguing typing experience, which I’ll discuss in detail shortly.</p><p>Thanks to the aluminum case, the Magi75 doesn’t have any flex in its body, and the keys don’t dip far into the PCB — something I’ve experienced with all-plastic decks. You also get double-shot PBT keycaps with anti-oil coating, and these are more wear- and tear-resistant than their ABS counterparts.</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="sKfutvygGcJetYoVif3M5d" name="IQUNIX_MG75_Mechanical_keyboard_008.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/sKfutvygGcJetYoVif3M5d-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Magi75 is a very good-looking keyboard too. You can get in black or white, with either pinkish-red or gold accents gracing the keycaps. It looks minimalist and low-key, and features customizable RGB lighting that doesn’t look too garish or loud. 10/10 from me in the design and build department, no notes.</p><h2 id="highly-portable">Highly portable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="npWLVbdyzgEMA9EbFTLEGd" name="IQUNIX_MG75_Mechanical_keyboard_011.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/npWLVbdyzgEMA9EbFTLEGd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 because the IQUNIX Magi75 features an aluminum case doesn’t mean you should assume it isn’t portable. The Magi75 is a low-profile 75% keyboard which is highly compact as it omits media keys and the number pad. It measures 12.46 x 4.96 x 0.44 inches so it doesn’t take up too much space on your desk or in your bag, and it’s more compact than both the NuPhy Air75 V3 and the Lofree Flow 2.</p><p>The Magi75 weighs 2.08lbs, and I didn’t feel like it was weighing me down when I was traveling with it in my backpack to and from work. It’s slightly heavier than the NuPhy Air75 V3 (1.65lbs) and the Lofree Flow 2 (1.64lbs), though, but the difference is hardly noticeable.</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="jYaicsZQeKLRAXNuT8haHc" name="IQUNIX_MG75_Mechanical_keyboard_003.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/jYaicsZQeKLRAXNuT8haHc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 to travel with the Magi75, the keyboard comes with… a carrying case included in the box. The case itself is bulky and can take up a bit of space in your bag, and personally, I don’t know who’s packing their keyboard in a separate case. But you might find some use for it! And it gives you a pocket to stash the printed manual in and trust me, you’ll need it — more on that soon.</p><h2 id="beautiful-to-type-on">Beautiful to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XfzYPGc9efgxgs6AZouHxd" name="IQUNIX_MG75_Mechanical_keyboard_010.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/XfzYPGc9efgxgs6AZouHxd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 someone whose bread and butter relies on writing nearly 10,000 words a week, having a comfortable keyboard I can type on all day long is high on my list of priorities. The IQUNIX Magi75 is just that. As is the nature of low-profile keyboards, the Magi75 uses low-profile switches with lower actuation points, so they’re easier and quicker to activate compared to standard-profile ones.</p><p>At the time of writing, the Magi75 comes fitted with one set of stock switches only: the Kailh Gold Red mechanicals. No word of a lie, I absolutely <em>adore </em>these switches. Typing on them feels like my fingers are bouncing off clouds. They require just 40g of force to activate, and they have a pre-travel distance of 1.2mm (2.8mm before bottoming out) so you don’t even need to press them all the way down to activate them. These switches are lighter and less fatiguing than the Kailh White Rains in the Hexgears Immersion A3 TKL (50g) and the Gateron LP 2.0 Reds in the Epomaker Luma84 (50g).</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="HHHz2YAiPSayLyK5fACuVc" name="IQUNIX_MG75_Mechanical_keyboard_006.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/HHHz2YAiPSayLyK5fACuVc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Magi75’s shallow depth also means that I don’t need to raise my wrists too much to reach the keys, and my hands sit closer to the surface of my desk, which essentially eliminates the need for a dedicated wrist rest. This makes the keyboard ergonomically-friendly, and having typed on it for five days’ worth of work, I can confirm that it’s <em>extremely </em>comfortable. My hands and fingers never once felt fatigued.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>IQUNIX Magi75</p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Turtle Beach Vulcan II Max</a></p></td><td  ><p>93</p></td><td  ><p>91.85%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a></p></td><td  ><p>94</p></td><td  ><p>92.3%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.39</p></td><td  ><p>91.14%</p></td></tr></tbody></table></div><p>To test how the Magi75 fared against other mechanical keyboards I’ve tested recently, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>. Right off the bat, I achieved 99 words per minute with 91% accuracy. As you can see in the table above, my speed was faster than usual, and though my accuracy took a slight dip, 91% is still not too shabby.</p><p>Oh, also, the Magi75 sounds <em>great</em>. The Kailh Gold Reds don’t sound too thocky, like the Wisterias in the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-p65-review">Epomaker P65</a> ($119), and they aren’t too quiet either, like the Kailh White Rains in the Hexgears Immersion A3 TKL. The keyboard sounds soft and creamy, and it feels like music to my ears. You won’t be disturbing your coworkers as you type away.</p><h2 id="decent-gaming-performance-too">Decent gaming performance, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XNFwxZx3fwVJ3dauv9ViEc" name="IQUNIX_MG75_Mechanical_keyboard_015.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/XNFwxZx3fwVJ3dauv9ViEc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Similar to the Lofree Flow84 and the Epomaker Luma84, the IQUNIX Magi75 is a productivity-first keyboard, and it isn’t intended for serious gaming. However, if you want to play a couple rounds of Counter-Strike 2 or drive a lap around Silverstone in F1 25 after work, the Magi75 should suffice.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>Over wired and 2.4GHz (using the included dongle), the keyboard has a 1,000Hz polling rate which drops to 125Hz over Bluetooth. For casual gamers, this should be more than plenty. I didn’t notice any latency issues or missed inputs. I found the switches to be super responsive too, and of course, the keyboard’s low-profile design felt mighty comfortable for all-day play. It’s worth checking out a low-profile Hall Effect keyboard, like the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air60-he-tiny-profile-huge-performance">NuPhy Air60 HE</a> ($139), if you’re a serious or competitive player.</p><h2 id="web-based-companion-software-4">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/6rAr4PboaXB5TzwcAjEnqn-1920-80.jpg" alt="Iqunix Magi75 keyboard being customized via VIA web app" /><figcaption><small role="credit">VIA / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/9pf3AS9gxueVCk7FqFX9qn-1920-80.jpg" alt="Iqunix Magi75 keyboard being customized via VIA web app" /><figcaption><small role="credit">VIA / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/7qCiJJzq69fjD5GixqUUon-1920-80.jpg" alt="Iqunix Magi75 keyboard being customized via VIA web app" /><figcaption><small role="credit">VIA / Tom's Guide</small></figcaption></figure></figure><p>I love it when keyboards and other peripherals don’t ask me to download proprietary third-party software or bloatware onto my laptop. To customize the IQUNIX Magi75, all you need to do is connect the keyboard to your machine and open the <a href="https://www.usevia.app/" target="_blank" rel="nofollow">VIA web app </a>in your browser. <a href="https://iqunix.com/pages/iqunix-super-custom-keyboard-kit-user-guide" target="_blank" rel="nofollow">Download the board’s JSON file</a>, upload it to VIA, and you’re good to go.</p><p>Customization via VIA (heh) is extremely straightforward, as the app gives you quick access to remapping keys, programming macros, adjusting the RGB effects and brightness, and updating/flashing the Magi75’s firmware. VIA sports a clean and user-friendly interface thanks to the omission of unnecessary pop-ups and ads. As a productivity-focused keyboard, you don’t really need more options in terms of customization.</p><h2 class="article-body__section" id="section-iqunix-magi75-review-the-downs"><span>IQUNIX Magi75 review: The downs</span></h2><p>Although there are many things the keyboard does well, the IQUNIX Magi75 sadly has some considerable drawbacks, such as confusing key combos, non-adjustable height, and very limited stock switch choice.</p><h2 id="confusing-key-combos-for-every-setting">Confusing key combos for every setting</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NJd22yMMzwTPJhnpMpsB7e" name="IQUNIX_MG75_Mechanical_keyboard_017.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/NJd22yMMzwTPJhnpMpsB7e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest gripes with the IQUNIX Magi75 is its lack of physical toggles to change settings. You need to rely on key combinations to change, well, <em>any </em>setting you want to. IQUNIX happily includes a physical manual in the box, and you can access a digital copy through the <a href="https://iqunix.com/blogs/news/magi75-96-iso" target="_blank" rel="nofollow">official website</a>.</p><p>But the key combos are quite annoying. Powering up the keyboard for the first time is fine because all you need to do is plug it in, but connecting it wirelessly is a pain — at least I found it to be. In my testing, when I followed the instructions to pair the Magi75 with my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> over Bluetooth, it took a good three or four tries to get it to work. With the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Dell Alienware 16X Aurora</a>, it was a similar story.</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="prJxnuMAyP2v4LYvFCzVkc" name="IQUNIX_MG75_Mechanical_keyboard_016.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/prJxnuMAyP2v4LYvFCzVkc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 could have easily been solved by placing a couple of physical toggles on the keyboard’s body but it’s difficult to do so on aluminum low-profile keyboards, and it’s something I haven’t seen on rivals either. My issue is that the combinations are finicky and didn’t always register at first when I was testing the keyboard, and I hope IQUNIX fixes this problem in the future.</p><h2 id="height-can-t-be-adjusted">Height can’t be adjusted</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="cD6aN87jeXBBJfaJ6uigYd" name="IQUNIX_MG75_Mechanical_keyboard_012.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/cD6aN87jeXBBJfaJ6uigYd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 drawback of the IQUNIX Magi75 isn’t exclusive to it — it’s a drawback of most aluminum keyboards. The Magi75 doesn’t have feet on its underside, which means its height can’t be adjusted. While it isn’t impossible to fit adjustable legs to an aluminum case as compared to a plastic one, it makes machining and manufacturing much trickier. We’ve seen this with the Epomaker Luma84 and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q3-ultra-8k-review">Keychron Q3 Ultra 8K</a> ($229), to give a couple examples. But a keyboard like the Lofree Flow 2 has managed it, so it would have been nice to see IQUNIX implement something similar.</p><p>If you want a great low-profile keyboard whose height can be adjusted, I’d recommend the Hexgears Immersion A3 TKL, a mech deck that I’ve been using for over a year when I work from home. The NuPhy Air75 V3 and the <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99) are superb alternatives too, but you’d be sacrificing the Magi75’s aluminum build for the latter.</p><h2 id="very-limited-stock-switch-choice-2">(Very) limited stock switch choice</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8n9zN9HxpoYV2NohEGUWTd" name="IQUNIX_MG75_Mechanical_keyboard_009.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/8n9zN9HxpoYV2NohEGUWTd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 alluded to earlier, the IQUNIX Magi75 comes with only one set of stock switches, and that’s the Gold Reds. The Gold Reds are outstanding, especially for those who want tactile feedback without obnoxious clickiness, but you can’t get any other switches, which is disappointing. Now, the Magi75 is hot-swappable, but Iquinix doesn’t specify compatibility, and it doesn’t sell any low-profile switches through its website. Weird.</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="hJcfroYcAcnTYh7Loc9Mrc" name="IQUNIX_MG75_Mechanical_keyboard_013.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/hJcfroYcAcnTYh7Loc9Mrc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 pulled a switch out using the included switch puller to inspect it, and they’re south-facing switches with a 3-pin setup. I then reached out to an IQUNIX representative who confirmed that the Magi75 is, at the time of writing, compatible with the Kailh 1353 series, and that “any variant based on this model will work.” You need to buy these separately as you can’t get them as stock switches when ordering the keyboard, which is a shame (and an added expense).</p><h2 id="an-expensive-entry-in-a-crowded-market">An expensive entry in a crowded market</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="v79ZfVeKsvhkGX5y5BX6kd" name="IQUNIX_MG75_Mechanical_keyboard_002.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/v79ZfVeKsvhkGX5y5BX6kd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Technology and keyboards have come leaps and bounds and today, you don’t need to spend hundreds and hundreds to get a fantastic low-profile keyboard. Though the IQUNIX Magi75 is excellent in its own right, I’m not entirely sure the asking price is justified. The Magi75 retails for <a href="https://www.amazon.com/IQUNIX-Magi75-Mechanical-Hot-Swappable-Customizable/dp/B0FK4WM767/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/Mechanical-Keyboard-Tri-Mode-Hot-Swappable-Customizable/dp/B0FL2YVN4C/" target="_blank" rel="nofollow">£195 at Amazon</a> — and that’s for the standard 75% variant. Its top line configuration, the Magi96 Pro, costs a whopping <a href="https://www.amazon.com/IQUNIX-Magi96-Pro-Hot-Swappable-Customizable/dp/B0G2XZKTQY/" target="_blank" rel="nofollow">$209</a>. That’s a <em>lot </em>of money.</p><p>If the Magi75 didn’t face such fierce competition, maybe that would be justified — none the wiser, right? But alternatives, like the <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-lite84-review">Lofree Flow Lite84</a> ($109), the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-luma84-review">Epomaker Luma84</a> ($109) and the Hexgears Immersion A3 TKL ($129), cost less and offer pretty much the same (or similar) specs and features. Oh, and the Immersion A3 TKL features a TFT screen for on-the-fly customization.</p><p>Unless you’re sold on the Magi75’s design — which is stunning, in my opinion — there isn’t much else unique about the keyboard that you won’t find anywhere else for similar or less money.</p><h2 class="article-body__section" id="section-iqunix-magi75-review-verdict"><span>IQUNIX Magi75 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="9z9dpRFQVRJ3RydEUtPfzd" name="IQUNIX_MG75_Mechanical_keyboard_005.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/9z9dpRFQVRJ3RydEUtPfzd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 it’s a wonderful typing experience you’re after, the IQUNIX Magi75 is an excellent choice. Its Gold Red switches are easy to activate, and the low-profile design offers a comfortable typing experience for all-day long work. The keyboard is designed beautifully, and its aluminum build makes it quite durable. Web-based software doesn’t give you much to complain about either.</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="WCy6j2BrzpLQ8YeGBnmhkc" name="IQUNIX_MG75_Mechanical_keyboard_014.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/WCy6j2BrzpLQ8YeGBnmhkc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 is the Magi75 worth the premium? I’m not entirely convinced, as its limited stock switch choice, non-adjustable height and confusing key combos leave a lot to be desired. The keyboard also faces fierce competition from the likes of Lofree and Epomaker who offer low-profile decks with the same features for either the same or less.</p><p>Don’t mind those drawbacks? Then the Magi75 could be a great addition to your productivity-focused setup. </p> ]]></dc:content>
                                                                                                                                            <link>https://www.tomsguide.com/computing/keyboards/iqunix-magi75-review</link>
                                                                            <description>
                            <![CDATA[ The IQUNIX Magi75 is a comfortable low-profile mechanical keyboard that offers a beautiful typing experience, but its flaws hold it back from being great. ]]>
                                                                                                            </description>
                                                                                                                                <guid isPermaLink="false">qYEQoVxJ4yUkC3xkiY3CGX</guid>
                                                                                                <enclosure url="https://cdn.mos.cms.futurecdn.net/P8hZT4iGuw5szcjMejusTc-1920-80.jpg" type="image/jpeg" length="0"></enclosure>
                                                                        <pubDate>Fri, 24 Apr 2026 12:47:31 +0000</pubDate>                                                                                                                                <updated>Tue, 09 Jun 2026 10:32:31 +0000</updated>
                                                                                                                                            <category><![CDATA[Keyboards]]></category>
                                                    <category><![CDATA[Computing]]></category>
                                                    <category><![CDATA[Computing Peripherals]]></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/7SPsEgwiRa9MdL3BPuVJUW-320-70.jpg ]]></dc:source>
                                                                <dc:description><![CDATA[ &lt;p&gt;Nikita is a Senior Writer on the Reviews team at Tom&amp;#39;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&amp;#39;s also a licensed drone pilot and cameras expert so you&amp;#39;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/P8hZT4iGuw5szcjMejusTc-1920-80.jpg">
                                                            <media:credit><![CDATA[Tom&#039;s Guide]]></media:credit>
                                                                                                                                                                                                                                    <media:description><![CDATA[A black Iqunix Magi75 mechanical keyboard]]></media:description>                                                            <media:text><![CDATA[A black Iqunix Magi75 mechanical keyboard]]></media:text>
                                <media:title type="plain"><![CDATA[A black Iqunix Magi75 mechanical keyboard]]></media:title>
                                                    </media:content>
                                                    <media:thumbnail url="https://cdn.mos.cms.futurecdn.net/P8hZT4iGuw5szcjMejusTc-1920-80.jpg" />
                                                                                                                                                                    <content:encoded >
                            <![CDATA[
                            <article>
                                <p>I’ve tested a lot of keyboards as a reviews writer. Last I counted, I’d reviewed over 80 keyboards — magnetic and mechanical. The most recent deck I’ve tested is the IQUNIX Magi75, a mechanical keyboard with beautiful Kailh Gold Red switches which are a delight to type on. The keyboard sounds lovely with a creamy profile, and it looks great, sporting a minimalist design and an aluminum build. Customization is super easy thanks to web-based software, and the Magi75 is suited to casual gaming too.</p><p>But the Magi75 isn’t perfect, and given how much fun the typing experience is, I was disappointed by its flaws. Very limited stock switch choice and the non-adjustable height are considerable drawbacks, as are the confusing key combos for changing settings and the premium price.</p><p>To find out if it’s one of the <a href="https://www.tomsguide.com/best-picks/best-mechanical-keyboards">best mechanical keyboards</a> for you, read my full IQUNIX Magi75 review.</p><h2 class="article-body__section" id="section-iqunix-magi75-review-specs"><span>IQUNIX Magi75 review: Specs</span></h2><div ><table><thead><tr><th class="firstcol " ><p><strong>Specs</strong></p></th><th  ><p><strong>IQUNIX Magi75</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p><strong>Price</strong></p></td><td  ><p><a href="https://www.amazon.com/IQUNIX-Magi75-Mechanical-Hot-Swappable-Customizable/dp/B0FK4WM767/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/Mechanical-Keyboard-Tri-Mode-Hot-Swappable-Customizable/dp/B0FL2YVN4C/" target="_blank" rel="nofollow">£195</a></p></td></tr><tr><td class="firstcol " ><p><strong>Switches</strong></p></td><td  ><p>Kailh Gold Red (linear) mechanical</p></td></tr><tr><td class="firstcol " ><p><strong>Keycaps</strong></p></td><td  ><p>Double-shot PBT</p></td></tr><tr><td class="firstcol " ><p><strong>Construction</strong></p></td><td  ><p>Aluminum + ABS plastic</p></td></tr><tr><td class="firstcol " ><p><strong>Mount</strong></p></td><td  ><p>Le-tray</p></td></tr><tr><td class="firstcol " ><p><strong>Layout</strong></p></td><td  ><p>75%, 84 keys</p></td></tr><tr><td class="firstcol " ><p><strong>Operating system</strong></p></td><td  ><p>macOS, Windows</p></td></tr><tr><td class="firstcol " ><p><strong>Backlighting</strong></p></td><td  ><p>Per-key RGB</p></td></tr><tr><td class="firstcol " ><p><strong>Polling rate</strong></p></td><td  ><p>1,000Hz (wired, 2.4GHz); 125Hz (Bluetooth)</p></td></tr><tr><td class="firstcol " ><p><strong>Paired devices max</strong></p></td><td  ><p>3</p></td></tr><tr><td class="firstcol " ><p><strong>Connectivity</strong></p></td><td  ><p>Bluetooth, 2.4GHz dongle, wired</p></td></tr><tr><td class="firstcol " ><p><strong>Battery</strong></p></td><td  ><p>4,000mAh</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated, RGB off)</strong></p></td><td  ><p>500 hours</p></td></tr><tr><td class="firstcol " ><p><strong>Battery life (rated, RGB on)</strong></p></td><td  ><p>10 hours (all lighting); 10.5 hours (backlight only)</p></td></tr><tr><td class="firstcol " ><p><strong>Measurements</strong></p></td><td  ><p>12.8 x 5.3 x 1.5 inches</p></td></tr><tr><td class="firstcol " ><p><strong>Weight</strong></p></td><td  ><p>2.08lbs</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-iqunix-magi75-review-cheat-sheet"><span>IQUNIX Magi75 review: Cheat sheet</span></h2><ul><li><strong>What is it? </strong>A low-profile mechanical keyboard with a 75% layout</li><li><strong>Who is it for? </strong>Hardcore typists who want a thoroughly responsive (and musically sound) typing experience</li><li><strong>How much does it cost? </strong>The IQUNIX Magi75 is available for <a href="https://www.amazon.com/IQUNIX-Magi75-Mechanical-Hot-Swappable-Customizable/dp/B0FK4WM767/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/Mechanical-Keyboard-Tri-Mode-Hot-Swappable-Customizable/dp/B0FL2YVN4C/" target="_blank" rel="nofollow">£195</a></li><li><strong>What do we like? </strong>The high quality build, lovely design, comfortable low-profile design, typing experience, and user-friendly companion software</li><li><strong>What don’t we like? </strong>The confusing key combos for settings, non-adjustable height, limited stock switch choice, and premium price</li></ul><h2 class="article-body__section" id="section-iqunix-magi75-review-the-ups"><span>IQUNIX Magi75 review: The ups</span></h2><p>From its exceptional build quality and lovely typing feel and sound, to its user-friendly web-based software, the IQUNIX Magi75 is a winner.</p><h2 id="built-like-a-gorgeous-tank">Built like a (gorgeous) tank</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="TpStcnAqUpErD9evzsP8Kc" name="IQUNIX_MG75_Mechanical_keyboard_004.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/TpStcnAqUpErD9evzsP8Kc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 IQUNIX Magi75 is one of the best built keyboards I’ve had the pleasure of using or testing. Utilizing a combination of plastic and CNC machined aluminum, this thing is built like a tank. The aluminum case feels premium and high-quality, and is scratch-resistant, so you can rest assured that it won’t get too damaged over time.</p><p>Because it features an aluminum case and is a low-profile keyboard, the Magi75’s closest competitors are the <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-2-review">Lofree Flow 2</a> ($149), the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air75-v3-review">NuPhy Air75 V3</a> ($139) and the <a href="https://www.tomsguide.com/computing/peripherals/hexgears-immersion-a3-tkl-review">Hexgears Immersion A3 TKL</a> ($129). The first two feature all-aluminum bottom cases while the Hexgears uses a combination of aluminum and plastic.</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="A9qK4ukXD7Ej35QRaZ9VUd" name="IQUNIX_MG75_Mechanical_keyboard_007.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/A9qK4ukXD7Ej35QRaZ9VUd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Also aiding the Magi75’s structural integrity is the le-tray mount design, which has been developed by IQUNIX and features four PCB support arms and silicone rings, as well as several layers of sound-dampening foam. The le-tray mount design, according to IQUNIX, “provides more bottom-out space within the compact low-profile keyboard layout.” In turn, it delivers a soft typing feel and a creamy sound profile. All of this also contributes to a comfortable and non-fatiguing typing experience, which I’ll discuss in detail shortly.</p><p>Thanks to the aluminum case, the Magi75 doesn’t have any flex in its body, and the keys don’t dip far into the PCB — something I’ve experienced with all-plastic decks. You also get double-shot PBT keycaps with anti-oil coating, and these are more wear- and tear-resistant than their ABS counterparts.</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="sKfutvygGcJetYoVif3M5d" name="IQUNIX_MG75_Mechanical_keyboard_008.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/sKfutvygGcJetYoVif3M5d-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Magi75 is a very good-looking keyboard too. You can get in black or white, with either pinkish-red or gold accents gracing the keycaps. It looks minimalist and low-key, and features customizable RGB lighting that doesn’t look too garish or loud. 10/10 from me in the design and build department, no notes.</p><h2 id="highly-portable">Highly portable</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="npWLVbdyzgEMA9EbFTLEGd" name="IQUNIX_MG75_Mechanical_keyboard_011.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/npWLVbdyzgEMA9EbFTLEGd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 because the IQUNIX Magi75 features an aluminum case doesn’t mean you should assume it isn’t portable. The Magi75 is a low-profile 75% keyboard which is highly compact as it omits media keys and the number pad. It measures 12.46 x 4.96 x 0.44 inches so it doesn’t take up too much space on your desk or in your bag, and it’s more compact than both the NuPhy Air75 V3 and the Lofree Flow 2.</p><p>The Magi75 weighs 2.08lbs, and I didn’t feel like it was weighing me down when I was traveling with it in my backpack to and from work. It’s slightly heavier than the NuPhy Air75 V3 (1.65lbs) and the Lofree Flow 2 (1.64lbs), though, but the difference is hardly noticeable.</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="jYaicsZQeKLRAXNuT8haHc" name="IQUNIX_MG75_Mechanical_keyboard_003.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/jYaicsZQeKLRAXNuT8haHc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 to travel with the Magi75, the keyboard comes with… a carrying case included in the box. The case itself is bulky and can take up a bit of space in your bag, and personally, I don’t know who’s packing their keyboard in a separate case. But you might find some use for it! And it gives you a pocket to stash the printed manual in and trust me, you’ll need it — more on that soon.</p><h2 id="beautiful-to-type-on">Beautiful to type on</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XfzYPGc9efgxgs6AZouHxd" name="IQUNIX_MG75_Mechanical_keyboard_010.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/XfzYPGc9efgxgs6AZouHxd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 someone whose bread and butter relies on writing nearly 10,000 words a week, having a comfortable keyboard I can type on all day long is high on my list of priorities. The IQUNIX Magi75 is just that. As is the nature of low-profile keyboards, the Magi75 uses low-profile switches with lower actuation points, so they’re easier and quicker to activate compared to standard-profile ones.</p><p>At the time of writing, the Magi75 comes fitted with one set of stock switches only: the Kailh Gold Red mechanicals. No word of a lie, I absolutely <em>adore </em>these switches. Typing on them feels like my fingers are bouncing off clouds. They require just 40g of force to activate, and they have a pre-travel distance of 1.2mm (2.8mm before bottoming out) so you don’t even need to press them all the way down to activate them. These switches are lighter and less fatiguing than the Kailh White Rains in the Hexgears Immersion A3 TKL (50g) and the Gateron LP 2.0 Reds in the Epomaker Luma84 (50g).</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="HHHz2YAiPSayLyK5fACuVc" name="IQUNIX_MG75_Mechanical_keyboard_006.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/HHHz2YAiPSayLyK5fACuVc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 Magi75’s shallow depth also means that I don’t need to raise my wrists too much to reach the keys, and my hands sit closer to the surface of my desk, which essentially eliminates the need for a dedicated wrist rest. This makes the keyboard ergonomically-friendly, and having typed on it for five days’ worth of work, I can confirm that it’s <em>extremely </em>comfortable. My hands and fingers never once felt fatigued.</p><div ><table><thead><tr><th class="firstcol " ><p><strong>Keyboard</strong></p></th><th  ><p><strong>Type test speed (WPM)</strong></p></th><th  ><p><strong>Type test accuracy</strong></p></th></tr></thead><tbody><tr><td class="firstcol " ><p>IQUNIX Magi75</p></td><td  ><p>99</p></td><td  ><p>91%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/asus-tuf-gaming-k3-gen-ii-review">Asus TUF Gaming K3 Gen II</a></p></td><td  ><p>105</p></td><td  ><p>96.5%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/cherry-xtrfy-mx-10-1-wireless-review">Cherry XTRFY MX 10.1 Wireless</a></p></td><td  ><p>97</p></td><td  ><p>93.04%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/royal-kludge-l75-review">Royal Kludge L75</a></p></td><td  ><p>96</p></td><td  ><p>91.82%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/turtle-beach-vulcan-ii-max-review">Turtle Beach Vulcan II Max</a></p></td><td  ><p>93</p></td><td  ><p>91.85%</p></td></tr><tr><td class="firstcol " ><p><a href="https://www.tomsguide.com/computing/keyboards/keychron-v1-ultra-8k-review">Keychron V1 Ultra 8K</a></p></td><td  ><p>94</p></td><td  ><p>92.3%</p></td></tr><tr><td class="firstcol " ><p>Nikita’s rolling averages (all keyboards)</p></td><td  ><p>92.39</p></td><td  ><p>91.14%</p></td></tr></tbody></table></div><p>To test how the Magi75 fared against other mechanical keyboards I’ve tested recently, I took a typing test at <a href="http://10fastfingers.com" target="_blank">10fastfingers.com</a>. Right off the bat, I achieved 99 words per minute with 91% accuracy. As you can see in the table above, my speed was faster than usual, and though my accuracy took a slight dip, 91% is still not too shabby.</p><p>Oh, also, the Magi75 sounds <em>great</em>. The Kailh Gold Reds don’t sound too thocky, like the Wisterias in the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-p65-review">Epomaker P65</a> ($119), and they aren’t too quiet either, like the Kailh White Rains in the Hexgears Immersion A3 TKL. The keyboard sounds soft and creamy, and it feels like music to my ears. You won’t be disturbing your coworkers as you type away.</p><h2 id="decent-gaming-performance-too">Decent gaming performance, too</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="XNFwxZx3fwVJ3dauv9ViEc" name="IQUNIX_MG75_Mechanical_keyboard_015.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/XNFwxZx3fwVJ3dauv9ViEc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Similar to the Lofree Flow84 and the Epomaker Luma84, the IQUNIX Magi75 is a productivity-first keyboard, and it isn’t intended for serious gaming. However, if you want to play a couple rounds of Counter-Strike 2 or drive a lap around Silverstone in F1 25 after work, the Magi75 should suffice.</p><div  class="fancy-box"><div class="fancy_box-title">Tom's Guide reviews gaming platform</div><div class="fancy_box_body"><p class="fancy-box__body-text"><strong>Supplied by: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/" target="_blank" rel="nofollow"><strong>MSI</strong></a><strong> | Tom's Guide</strong><br><strong></strong><br><strong>CPU:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/AMD-7700X-16-Thread-Unlocked-Processor/dp/B0BBHHT8LY/" target="_blank" rel="nofollow">AMD Ryzen 7 7700X</a><strong> </strong>|<strong> Graphics card:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Graphics-Card/GeForce-RTX-5070-Ti-16G-VANGUARD-SOC" target="_blank" rel="nofollow">MSI RTX 5070 Ti 16GB Vanguard SOC</a><strong> </strong>|<strong> Motherboard: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/Motherboard/B850-GAMING-PLUS-WIFI" target="_blank" rel="nofollow">MSI B850E Gaming Plus WiFi</a><strong> </strong>|<strong> RAM: </strong><a data-analytics-id="inline-link" href="https://www.newegg.com/kingston-technology-corp-fury-renegade-32gb-ddr5-8000-cas-latency-cl38-desktop-memory-silver-black/p/N82E16820242829" target="_blank" rel="nofollow">Kingston Fury Renegade DDR5 32GB</a><strong> </strong>|<strong> Cooler:</strong> <a data-analytics-id="inline-link" href="https://www.amazon.com/Noctua-NH-U12S-chromax-Black-Single-Tower-Cooler/dp/B07Y88BNYZ" target="_blank">Noctua NH-U12S</a><strong> </strong>|<strong> PSU:</strong> <a data-analytics-id="inline-link" href="https://www.msi.com/Power-Supply/MEG-Ai1300P-PCIE5" target="_blank" rel="nofollow">MSI MEG Ai1300P PCIE5</a><strong> </strong>|<strong> Case: </strong><a data-analytics-id="inline-link" href="https://www.msi.com/PC-Case/MPG-GUNGNIR-110R" target="_blank" rel="nofollow">MSI MPG GUNGNIR 110R</a></p></div></div><p>Over wired and 2.4GHz (using the included dongle), the keyboard has a 1,000Hz polling rate which drops to 125Hz over Bluetooth. For casual gamers, this should be more than plenty. I didn’t notice any latency issues or missed inputs. I found the switches to be super responsive too, and of course, the keyboard’s low-profile design felt mighty comfortable for all-day play. It’s worth checking out a low-profile Hall Effect keyboard, like the <a href="https://www.tomsguide.com/computing/peripherals/nuphy-air60-he-tiny-profile-huge-performance">NuPhy Air60 HE</a> ($139), if you’re a serious or competitive player.</p><h2 id="web-based-companion-software-4">Web-based companion software</h2><figure role="gallery"><figure><img src="https://cdn.mos.cms.futurecdn.net/6rAr4PboaXB5TzwcAjEnqn-1920-80.jpg" alt="Iqunix Magi75 keyboard being customized via VIA web app" /><figcaption><small role="credit">VIA / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/9pf3AS9gxueVCk7FqFX9qn-1920-80.jpg" alt="Iqunix Magi75 keyboard being customized via VIA web app" /><figcaption><small role="credit">VIA / Tom's Guide</small></figcaption></figure><figure><img src="https://cdn.mos.cms.futurecdn.net/7qCiJJzq69fjD5GixqUUon-1920-80.jpg" alt="Iqunix Magi75 keyboard being customized via VIA web app" /><figcaption><small role="credit">VIA / Tom's Guide</small></figcaption></figure></figure><p>I love it when keyboards and other peripherals don’t ask me to download proprietary third-party software or bloatware onto my laptop. To customize the IQUNIX Magi75, all you need to do is connect the keyboard to your machine and open the <a href="https://www.usevia.app/" target="_blank" rel="nofollow">VIA web app </a>in your browser. <a href="https://iqunix.com/pages/iqunix-super-custom-keyboard-kit-user-guide" target="_blank" rel="nofollow">Download the board’s JSON file</a>, upload it to VIA, and you’re good to go.</p><p>Customization via VIA (heh) is extremely straightforward, as the app gives you quick access to remapping keys, programming macros, adjusting the RGB effects and brightness, and updating/flashing the Magi75’s firmware. VIA sports a clean and user-friendly interface thanks to the omission of unnecessary pop-ups and ads. As a productivity-focused keyboard, you don’t really need more options in terms of customization.</p><h2 class="article-body__section" id="section-iqunix-magi75-review-the-downs"><span>IQUNIX Magi75 review: The downs</span></h2><p>Although there are many things the keyboard does well, the IQUNIX Magi75 sadly has some considerable drawbacks, such as confusing key combos, non-adjustable height, and very limited stock switch choice.</p><h2 id="confusing-key-combos-for-every-setting">Confusing key combos for every setting</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="NJd22yMMzwTPJhnpMpsB7e" name="IQUNIX_MG75_Mechanical_keyboard_017.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/NJd22yMMzwTPJhnpMpsB7e-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 biggest gripes with the IQUNIX Magi75 is its lack of physical toggles to change settings. You need to rely on key combinations to change, well, <em>any </em>setting you want to. IQUNIX happily includes a physical manual in the box, and you can access a digital copy through the <a href="https://iqunix.com/blogs/news/magi75-96-iso" target="_blank" rel="nofollow">official website</a>.</p><p>But the key combos are quite annoying. Powering up the keyboard for the first time is fine because all you need to do is plug it in, but connecting it wirelessly is a pain — at least I found it to be. In my testing, when I followed the instructions to pair the Magi75 with my <a href="https://www.tomsguide.com/reviews/macbook-air-2022-m2">MacBook Air M2</a> over Bluetooth, it took a good three or four tries to get it to work. With the <a href="https://www.tomsguide.com/computing/gaming-laptops/alienware-16x-aurora-review">Dell Alienware 16X Aurora</a>, it was a similar story.</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="prJxnuMAyP2v4LYvFCzVkc" name="IQUNIX_MG75_Mechanical_keyboard_016.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/prJxnuMAyP2v4LYvFCzVkc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 could have easily been solved by placing a couple of physical toggles on the keyboard’s body but it’s difficult to do so on aluminum low-profile keyboards, and it’s something I haven’t seen on rivals either. My issue is that the combinations are finicky and didn’t always register at first when I was testing the keyboard, and I hope IQUNIX fixes this problem in the future.</p><h2 id="height-can-t-be-adjusted">Height can’t be adjusted</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="cD6aN87jeXBBJfaJ6uigYd" name="IQUNIX_MG75_Mechanical_keyboard_012.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/cD6aN87jeXBBJfaJ6uigYd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 drawback of the IQUNIX Magi75 isn’t exclusive to it — it’s a drawback of most aluminum keyboards. The Magi75 doesn’t have feet on its underside, which means its height can’t be adjusted. While it isn’t impossible to fit adjustable legs to an aluminum case as compared to a plastic one, it makes machining and manufacturing much trickier. We’ve seen this with the Epomaker Luma84 and the <a href="https://www.tomsguide.com/computing/keyboards/keychron-q3-ultra-8k-review">Keychron Q3 Ultra 8K</a> ($229), to give a couple examples. But a keyboard like the Lofree Flow 2 has managed it, so it would have been nice to see IQUNIX implement something similar.</p><p>If you want a great low-profile keyboard whose height can be adjusted, I’d recommend the Hexgears Immersion A3 TKL, a mech deck that I’ve been using for over a year when I work from home. The NuPhy Air75 V3 and the <a href="https://www.tomsguide.com/computing/keyboards/nuphy-node75-review">NuPhy Node75</a> ($99) are superb alternatives too, but you’d be sacrificing the Magi75’s aluminum build for the latter.</p><h2 id="very-limited-stock-switch-choice-2">(Very) limited stock switch choice</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="8n9zN9HxpoYV2NohEGUWTd" name="IQUNIX_MG75_Mechanical_keyboard_009.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/8n9zN9HxpoYV2NohEGUWTd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 alluded to earlier, the IQUNIX Magi75 comes with only one set of stock switches, and that’s the Gold Reds. The Gold Reds are outstanding, especially for those who want tactile feedback without obnoxious clickiness, but you can’t get any other switches, which is disappointing. Now, the Magi75 is hot-swappable, but Iquinix doesn’t specify compatibility, and it doesn’t sell any low-profile switches through its website. Weird.</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="hJcfroYcAcnTYh7Loc9Mrc" name="IQUNIX_MG75_Mechanical_keyboard_013.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/hJcfroYcAcnTYh7Loc9Mrc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 pulled a switch out using the included switch puller to inspect it, and they’re south-facing switches with a 3-pin setup. I then reached out to an IQUNIX representative who confirmed that the Magi75 is, at the time of writing, compatible with the Kailh 1353 series, and that “any variant based on this model will work.” You need to buy these separately as you can’t get them as stock switches when ordering the keyboard, which is a shame (and an added expense).</p><h2 id="an-expensive-entry-in-a-crowded-market">An expensive entry in a crowded market</h2><figure class="van-image-figure  inline-layout" data-bordeaux-image-check ><div class='image-full-width-wrapper'><div class='image-widthsetter' style="max-width:1920px;"><p class="vanilla-image-block" style="padding-top:56.25%;"><img id="v79ZfVeKsvhkGX5y5BX6kd" name="IQUNIX_MG75_Mechanical_keyboard_002.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/v79ZfVeKsvhkGX5y5BX6kd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></p></div></div><figcaption itemprop="caption description" class=" inline-layout"><span class="credit" itemprop="copyrightHolder">(Image credit: Tom's Guide)</span></figcaption></figure><p>Technology and keyboards have come leaps and bounds and today, you don’t need to spend hundreds and hundreds to get a fantastic low-profile keyboard. Though the IQUNIX Magi75 is excellent in its own right, I’m not entirely sure the asking price is justified. The Magi75 retails for <a href="https://www.amazon.com/IQUNIX-Magi75-Mechanical-Hot-Swappable-Customizable/dp/B0FK4WM767/" target="_blank" rel="nofollow">$169</a> / <a href="https://www.amazon.co.uk/Mechanical-Keyboard-Tri-Mode-Hot-Swappable-Customizable/dp/B0FL2YVN4C/" target="_blank" rel="nofollow">£195 at Amazon</a> — and that’s for the standard 75% variant. Its top line configuration, the Magi96 Pro, costs a whopping <a href="https://www.amazon.com/IQUNIX-Magi96-Pro-Hot-Swappable-Customizable/dp/B0G2XZKTQY/" target="_blank" rel="nofollow">$209</a>. That’s a <em>lot </em>of money.</p><p>If the Magi75 didn’t face such fierce competition, maybe that would be justified — none the wiser, right? But alternatives, like the <a href="https://www.tomsguide.com/computing/keyboards/lofree-flow-lite84-review">Lofree Flow Lite84</a> ($109), the <a href="https://www.tomsguide.com/computing/keyboards/epomaker-luma84-review">Epomaker Luma84</a> ($109) and the Hexgears Immersion A3 TKL ($129), cost less and offer pretty much the same (or similar) specs and features. Oh, and the Immersion A3 TKL features a TFT screen for on-the-fly customization.</p><p>Unless you’re sold on the Magi75’s design — which is stunning, in my opinion — there isn’t much else unique about the keyboard that you won’t find anywhere else for similar or less money.</p><h2 class="article-body__section" id="section-iqunix-magi75-review-verdict"><span>IQUNIX Magi75 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="9z9dpRFQVRJ3RydEUtPfzd" name="IQUNIX_MG75_Mechanical_keyboard_005.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/9z9dpRFQVRJ3RydEUtPfzd-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 it’s a wonderful typing experience you’re after, the IQUNIX Magi75 is an excellent choice. Its Gold Red switches are easy to activate, and the low-profile design offers a comfortable typing experience for all-day long work. The keyboard is designed beautifully, and its aluminum build makes it quite durable. Web-based software doesn’t give you much to complain about either.</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="WCy6j2BrzpLQ8YeGBnmhkc" name="IQUNIX_MG75_Mechanical_keyboard_014.JPG" alt="A black Iqunix Magi75 mechanical keyboard" src="https://cdn.mos.cms.futurecdn.net/WCy6j2BrzpLQ8YeGBnmhkc-1920-80.jpg" mos="" align="middle" fullscreen="" width="1920" height="1080" attribution="" endorsement="" class="inline"></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 is the Magi75 worth the premium? I’m not entirely convinced, as its limited stock switch choice, non-adjustable height and confusing key combos leave a lot to be desired. The keyboard also faces fierce competition from the likes of Lofree and Epomaker who offer low-profile decks with the same features for either the same or less.</p><p>Don’t mind those drawbacks? Then the Magi75 could be a great addition to your productivity-focused setup. </p>
                                                            </article>
                            ]]>
                        </content:encoded>
                                                </item>
            </channel>
</rss>