PageSourceSearch

https://utc.com.kw/app.js

js utc.com.kw collected 2026-10-08 00:32:35 UTC 83,701 bytes, 1,063 lines download raw bytes

1'use strict';
2/* Union Trading Co. — faithful static rebuild of the Claude Design prototype. */
3(function () {
4
5  /* ---------- icon helpers ---------- */
6  const ic = p => '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">' + p + '</svg>';
7  const sic = p => '<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor">' + p + '</svg>';
8
9  /* ---------- crop table (from image-slot state; _applyView reproduction) ---------- */
10  const CROP = {
11    'div-elec': { s: 1, x: 23.44, y: 0 }, 'div-fmcg': { s: 1, x: 21.83, y: 0 }, 'div-cosmetics': { s: 1, x: 30.91, y: 0 }, 'div-fashion': { s: 1, x: 16.63, y: 0 }, 'div-health': { s: 1, x: 0, y: 0 }, 'div-commercial': { s: 1, x: 0, y: 0 },
12    'story-storefront': { s: 1, x: 0, y: 4.99 },
13    'ms-1966': { s: 1, x: 7.69, y: 0 }, 'ms-1970': { s: 1, x: 7.69, y: 0 }, 'ms-1969': { s: 1, x: 7.69, y: 0 },
14    'pimg-0': { s: 1, x: -41.94, y: 0 }, 'pimg-1': { s: 1, x: -34.51, y: 0 }, 'pimg-2': { s: 1, x: -26.52, y: 0 }, 'pimg-3': { s: 1, x: 0, y: 0 },
15    'hero-4': { s: 1, x: -23, y: 0 }  // Havit (slide 5) — pan right so the subject's face stays in frame
16  };
17
18  function layoutSlot(el) {
19    const img = el.querySelector('img');
20    if (!img || !img.naturalWidth) return;
21    const fw = el.clientWidth, fh = el.clientHeight;
22    if (!fw || !fh) return;
23    const iw = img.naturalWidth, ih = img.naturalHeight;
24    const fit = el.dataset.fit || 'cover';
25    const base = fit === 'contain' ? Math.min(fw / iw, fh / ih) : Math.max(fw / iw, fh / ih);
26    const c = CROP[el.dataset.slot] || { s: 1, x: 0, y: 0 };
27    const k = base * (c.s || 1);
28    const imgW = iw * k, imgH = ih * k;
29    // desired centre shift (px) from the stored crop, clamped so a cover image
30    // never reveals a gap — pan only as far as the available overflow allows.
31    let cx = (c.x || 0) / 100 * fw, cy = (c.y || 0) / 100 * fh;
32    const maxX = Math.max(0, (imgW - fw) / 2), maxY = Math.max(0, (imgH - fh) / 2);
33    cx = Math.max(-maxX, Math.min(maxX, cx));
34    cy = Math.max(-maxY, Math.min(maxY, cy));
35    img.style.width = (imgW / fw * 100) + '%';
36    img.style.height = (imgH / fh * 100) + '%';
37    img.style.left = 'calc(50% + ' + cx.toFixed(1) + 'px)';
38    img.style.top = 'calc(50% + ' + cy.toFixed(1) + 'px)';
39  }
40  function layoutAll() { document.querySelectorAll('.slot').forEach(layoutSlot); }
41  window.addEventListener('resize', layoutAll);
42  // Re-lay-out each slot whenever its box settles (zoom, 3D transforms, fonts,
43  // responsive) — guarantees cover images fully fill regardless of load timing.
44  const slotRO = ('ResizeObserver' in window) ? new ResizeObserver(es => es.forEach(e => layoutSlot(e.target))) : null;
45  function observeSlots() { if (slotRO) document.querySelectorAll('.slot').forEach(s => slotRO.observe(s)); }
46
47  // build a slot element with an image; relayout on load
48  function slotEl(id, src, fit) {
49    const d = document.createElement('div');
50    d.className = 'slot';
51    d.dataset.slot = id;
52    if (fit) d.dataset.fit = fit;
53    const img = document.createElement('img');
54    img.alt = '';
55    img.decoding = 'async';
56    img.onload = () => layoutSlot(d);
57    img.src = src;
58    d.appendChild(img);
59    return d;
60  }
61  function fillSlot(el, id, src, fit) {
62    el.dataset.slot = id;
63    if (fit) el.dataset.fit = fit;
64    const existing = el.querySelector('img');
65    if (existing && existing.getAttribute('src') === src) { layoutSlot(el); return; }
66    // Preload + decode the new image off-screen while the current one stays visible,
67    // then swap it in already sized. Without this, a fresh <img> paints at its natural
68    // (large) size for a frame before layoutSlot shrinks it — a flicker on manual switches.
69    let done = false;
70    const apply = () => {
71      if (done) return; done = true;
72      let img = el.querySelector('img');
73      if (!img) { img = document.createElement('img'); img.alt = ''; img.decoding = 'async'; el.appendChild(img); }
74      img.onload = () => layoutSlot(el);
75      img.src = src;
76      layoutSlot(el);
76              // src is cache-warm now → naturalWidth ready, size before paint
77      img.style.visibility = 'visible';
78    };
79    const next = new Image();
80    next.decoding = 'async';
81    next.onload = apply;
82    next.onerror = apply;
83    next.src = src;
84    if (next.complete && next.naturalWidth) apply();   // already cached & decoded
85  }
86
87  /* ---------- data ---------- */
88  const NAV = ['About Us', 'Our Divisions', 'Our Stores', 'Our Channels', 'Careers', 'Contact'];
89  const NAV_SOON = ['Careers'];
90  const heroImgs = ['uploads/Carousal4.webp', 'uploads/OGeneral.webp', 'uploads/cecotec.webp', 'uploads/changhong.webp', 'uploads/havit.webp'];
91  const heroPartners = ['GlemGas', 'General', 'Cecotec', 'CHiQ Changhong', 'Havit'];
92  const heroLabelLight = [true, false, false, true, true];  // dark label on the light General & Cecotec images; white on Changhong & Havit (dark scenes)
93
94  function heroPalette() {
95    const light = (bg, head, sub, kick, track, muted) => ({ heroBg: bg, heroHead: head, heroSub: sub, heroKick: kick, heroTrack: track, heroFill: head, heroMuted: muted, heroCtaBg: '#0d0d0d', heroCtaTx: '#fff' });
96    const dark = (bg, head, sub, kick, track, muted) => ({ heroBg: bg, heroHead: head, heroSub: sub, heroKick: kick, heroTrack: track, heroFill: head, heroMuted: muted, heroCtaBg: head, heroCtaTx: bg });
97    return [
98      dark('#182430', '#f2efe8', '#b6c0c8', '#9dafba', '#37485580', '#93a3ac'),   // GlemGas
99      light('#e6ddc9', '#2a2417', '#57503d', '#6b6250', '#c9c0a6', '#5c5446'),    // O General
100      light('#e8e3d9', '#2a2620', '#57524a', '#675f51', '#cbc4b4', '#585040'),    // Cecotec (warm bedroom)
101      dark('#101d33', '#eef2f6', '#aab6c6', '#93a3b6', '#2c3c5680', '#8fa0af'),    // CHiQ Changhong (starry navy)
102      light('#dcdedd', '#20262a', '#4e565a', '#5b6367', '#c1c7c6', '#525a5c'),    // Havit (cool misty forest)
103    ];
104  }
105
106  const brandNames = [
107    { name: 'Braun', src: 'assets/b1.png' }, { name: 'Angel Wear', src: 'assets/b2.png' }, { name: 'India Gate', src: 'assets/b3.png' },
108    { name: 'Hitachi', src: 'assets/b4.png' }, { name: 'Brandili', src: 'assets/b5.png' }, { name: 'Constance Carroll', src: 'assets/b6.png' },
109    { name: 'Glysolid', src: 'assets/b7.png' }, { name: 'Eagle Safes', src: 'assets/b8.png' }, { name: 'Prestige', src: 'assets/b9.png' }, { name: 'O General', src: 'assets/b10.png' },
110    { name: 'Daikin', src: 'img/brands-elec/daikin.webp' }, { name: 'Triumph', src: 'img/brands-aluna/triumph.webp' }, { name: 'Sloggi', src: 'img/brands-aluna/sloggi.webp' },
111    { name: 'Vileda', src: 'img/brands-elec/vileda.webp' }, { name: 'Cans', src: 'img/brands-elec/cans.webp' }, { name: 'Palladio', src: 'img/brands-elec/palladio.webp' }, { name: 'Vero Moda', src: 'img/brands-elec/vero-moda.webp' }
112  ];
113  function logoFor(name) {
114    const n = String(name).toLowerCase();
115    const f = brandNames.find(b => { const bn = b.name.toLowerCase(); return bn === n || bn.includes(n) || n.includes(bn); });
116    return f ? f.src : null;
117  }
118
119  const divData = [
120    { name: 'Union Electronics', href: 'union-electronics/', logo: 'img/Div/electronics-logo.webp', cardLogo: 'img/Div/Black.webp', cardLogoH: 'clamp(48px,6vw,74px)', tagline: 'Smarter Homes. Simpler Living.', icon: '<rect x="4" y="3" width="7" height="18" rx="1.5"></rect><circle cx="7.5" cy="8" r="1.4"></circle><rect x="13" y="3" width="7" height="18" rx="1.5"></rect><circle cx="16.5" cy="8" r="1.4"></circle>', desc: 'Recognizing the growing demand for premium electronic appliances, UNION strategically expanded its portfolio to offer a comprehensive range of world-renowned consumer electronics and home appliances. Through partnerships with leading global brands, the company has consistently delivered innovative, reliable, and energy-efficient solutions that enhance everyday living while meeting the evolving needs of customers across Kuwait.', mgr: 'Available on request', contact: 'Chat with Us', brands: ['Hitachi', 'Daikin', '+22 more'], slot: 'div-elec' },
121    { name: 'Aluna', href: 'aluna/', logo: 'img/Div/aluna-logo.webp', cardLogo: 'img/Div/aluna-logo.webp', cardLogoDark: true, tagline: 'Beauty in 
121Every Detail.', icon: '<path d="M9 21h6a2 2 0 002-2v-6a3 3 0 00-3-3h-1V7h-2v3H10a3 3 0 00-3 3v6a2 2 0 002 2z"></path><path d="M10 5h4M11 3h2"></path>', desc: 'Recognizing the growing demand for premium lingerie and shapewear, UNION developed a distinguished portfolio of internationally acclaimed lingerie and shapewear brands. By offering authentic, high-quality products, the company continues to deliver luxury, elegance, and confidence to customers throughout Kuwait.', mgr: 'Available on request', contact: 'Chat with Us', brands: ['Triumph', 'Sloggi', '+12 more'], slot: 'div-cosmetics' },
122    { name: 'FMCG', href: 'fmcg/', logo: 'img/Div/fmcg-logo.webp', cardLogo: 'img/Div/fmcg-logo.webp', cardLogoDark: true, tagline: 'Everyday Essentials, Delivered.', icon: '<circle cx="9" cy="20" r="1.2"></circle><circle cx="17" cy="20" r="1.2"></circle><path d="M2 4h2.5l2.5 12h10l2-8H6.2"></path>', desc: 'As consumer expectations for quality everyday essentials continued to rise, UNION expanded its portfolio to include a wide range of fast-moving consumer goods from leading international brands. By delivering trusted food, beverage, and household products, the company has become a reliable partner in meeting the daily needs of consumers.', mgr: 'Available on request', contact: 'Chat with Us', brands: ['Vileda', 'Cans', '+14 more'], slot: 'div-fmcg' },
123    { name: 'Labels', soon: true, logo: 'img/Div/labels-logo.webp', cardLogo: 'img/Div/labels-logo.webp', cardLogoDark: true, cardLogoH: 'clamp(48px,6vw,74px)', tagline: 'Timeless Everyday Style.', icon: '<circle cx="12" cy="5" r="2"></circle><path d="M12 7l7 5-2 2-2-1.5V21H9v-8.5L7 14l-2-2 7-5z"></path>', desc: 'With changing lifestyles and increasing demand for premium fashion and cosmetics, UNION established a strong presence by introducing internationally renowned fashion, cosmetics, and lifestyle brands. Through a carefully curated portfolio, the company continues to deliver quality, style, and exceptional retail experiences to customers across the region.', mgr: 'Available on request', contact: 'Chat with Us', brands: ['Palladio', 'Vero Moda', '+7 more'], slot: 'div-fashion' },
124    { name: 'Union Services', soon: true, logo: 'img/Div/services-logo.webp', cardLogo: 'img/Div/services-logo.webp', cardLogoDark: true, cardLogoH: 'clamp(44px,5.4vw,66px)', tagline: 'Caring for Every Family.', icon: '<path d="M12 20s-6.5-4.2-9-8.2C1.4 9 2.6 5.5 6 5.5c2 0 3.2 1.2 4 2.4.8-1.2 2-2.4 4-2.4 3.4 0 4.6 3.5 3 6.3-2.5 4-9 8.2-9 8.2z"></path>', desc: 'Union Services is dedicated to providing reliable and professional after-sales and technical services to ensure our customers get the best performance and value from their products. From pre-installation (site survey), installation, startup, and preventive maintenance to troubleshooting and repairs, our experienced service teams are committed to delivering quality service and a smooth customer experience.', mgr: 'Available on request', contact: 'Chat with Us', brands: [], services: ['Air Conditioning, HVAC & VRF Systems', 'Home Appliances & Built-in Systems', 'Audio/Video & Electronics Systems', 'Site Survey & Technical Assessment', 'Installation, Start-Up & Commissioning', 'Preventive Maintenance & AMC', 'Corrective Maintenance, Troubleshooting & Repair', 'Product Replacement & Upgrading', 'Genuine Spare Parts & Technical Support', 'Water Solutions Services'], slot: 'div-health' },
125    { name: 'Commercial AC Projects', soon: true, logo: 'img/Div/commercial-logo.webp', cardLogo: 'img/Div/commercial-logo.webp', cardLogoDark: true, cardLogoH: 'clamp(44px,5.2vw,64px)', tabW: '138px', tagline: 'Powerful, Efficient Cooling.', icon: '<rect x="3" y="5" width="18" height="8" rx="2"></rect><path d="M6.5 9h1M10 9h7.5M7 16c.2 1.4 1 1.8 1 3M12 16c.2 1.4 1 1.8 1 3M17 16c-.2 1.4-1 1.8-1 3"></path>', desc: "At the time of the company's inception there was a great demand in the market for climate control technology. UTC's answer to this demand was the creation of its very own line of climate control solution: General Air Conditioners.", descHtml: "At the time of the company's inception there was a great demand in the market for climate control technology. UTC's answer to this demand was the creation of its very own line of climate control <strong style=\"color:#161513;font-weight:700\">solution: General Air Conditioners.</strong>", mgr: 'Mr. Walid Khalid', contact: 'Chat with Us', brands: ['Daikin', 'Hitachi', 'General'], slot: 'div-commercial' }
126  ];
127
128  const storyStats = [
129    { n: '75+', l: 'YEARS IN BUSINESS', sub: 'Built on trust. Driven by excellence.', icon: ic('<rect x="3" y="7" width="18" height="13" rx="1"></rect><path d="M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2"></path>') },
130    { n: '80+', l: 'GLOBAL BRANDS', sub: 'Partnerships with world leaders.', icon: ic('<circle cx="12" cy="12" r="9"></circle><path d="M3 12h18M12 3c3 3.5 3 14.5 0 18M12 3c-3 3.5-3 14.5 0 18"></path>') },
131    { n: '500+', l: 'EMPLOYEES', sub: 'A team powering our promise.', icon: ic('<circle cx="9" cy="8" r="3"></circle><path d="M3 20c0-3 3-5 6-5s6 2 6 5"></path><path d="M16 6a3 3 0 010 6M21 20c0-2.5-2-4-4-4.5"></path>') },
132    { n: '10+', l: 'RETAIL OUTLETS', sub: 'Across Kuwait and beyond.', icon: ic('<path d="M3 9l1.5-5h15L21 9"></path><path d="M4 9v10a1 1 0 001 1h14a1 1 0 001-1V9"></path><path d="M3 9h18"></path><path d="M9 20v-6h6v6"></path>') }
133  ];
134  const storyExtra = ['uploads/city-showroom.webp', 'uploads/old-general-photo.webp'];
135
136  const plist = [
137    { name: 'Milton Lloyd', since: '1978', years: '44+', src: 'uploads/Mil-trimmed.webp', person: 'Peter Jackson', role: 'CEO', quote: 'Our mission with UTC branded perfumes, throughout the past 44 years, has (under Union Trading Company’s continuing unique and continuing vision / foresight and leadership) always been to offer to consumers with limited financial resources outstanding perfumery of the finest quality and at the lowest possible price. Since 1978, this unique and continuous perfumery cooperation between Union Trading Company and Milton-Lloyd has manufactured (in the UK) and distributed (initially in Kuwait and then throughout the world) more than 300 million bottles of UTC glass perfumery. A huge number. An unprecedented success. We believe that Milton-Lloyd’s cooperation and friendship with Union Trading Company is as strong or stronger today than it has ever been. And the future of UTC is bright.' },
138    { name: 'Glemgas', since: '1970', years: '50+', src: 'uploads/Glem-trimmed.webp', person: 'Marco Guerzoni', role: 'Marketing Director', quote: 'It has been a long journey of over 50 years under the guidance of UTC that helped us to be market leader in Kuwait. All of our achievements weren’t possible without the great support and effort of the entire UTC team. From the ones in the past who have made such a great story possible, to the current and future one that god willing they will certainly raise the bar to achieve what now seems impossible to our eyes. I personally want to thank all of UTC for their precious dedication and support, and am looking forward to celebrate our new impossible achievements.' },
139    { name: 'Jockey', since: '2003', years: '20+', src: 'uploads/Jock-trimmed.webp', person: 'Brand Team', role: 'Brand Partner', quote: 'Jockey International, Inc. has enjoyed a valuable working relationship with Union Trading Co. for the past 20 years. We value UTC’s loyal patronage over those years and the business relationship that has ensued because of our cooperation. We have watched Union Trading Co. grow into a thriving business and a business well-known throughout the Gulf region. We know Union Trading Co. has many options and we are pleased with the close relationship between our companies and look forward to many more years of serving Union Trading Co. We are committed to providing Union Trading Co. with quality service and the best products at a fair price. The cooperation between Jockey International, Inc. and Union Trading Co. has led to a respectful business relationship between our two companies. Jockey International, Inc. looks forward to many more years of our association with Union Trading Co. and wish you the success that you so richly deserve as you relaunch of your Corporate Website. Thank y
139ou, Union Trading Co. for giving Jockey International, Inc. the opportunity to serve your great company.' },
140  ];
141
142  const milestones = [
143    { year: '1949', title: 'First Showroom, Ahmadi', desc: 'Our journey began with a single showroom in Ahmadi, marking the start of Union Trading Co.', slot: 'ms-1949' },
144    { year: '1958', title: 'Kuwait City Showroom Launch', desc: 'We expanded our presence with the launch of our showroom in Kuwait City, bringing us closer to our customers.', slot: 'ms-1958' },
145    { year: '1966', title: 'Partnership with Gillette', desc: 'We partnered with Gillette to bring world-class grooming products to our customers.', slot: 'ms-1966' },
146    { year: '1969', title: 'Partnership with Glemgas', desc: 'We joined hands with Glemgas to bring premium Italian home appliances to our customers.', slot: 'ms-1969' },
147    { year: '1970', title: 'Partnership with General', desc: 'Joined hands with General to offer advanced and reliable air conditioning solutions.', slot: 'ms-1970' },
148    { year: '1977', title: 'Foundation of UTC Perfumes', desc: 'We ventured into beauty with the foundation of UTC Perfumes — quality fragrances for every lifestyle.', slot: 'ms-1977' },
149    { year: '1980', title: 'Salhiya Showroom', desc: 'We opened our Salhiya showroom, extending Union Trading Co.’s retail presence into the heart of Kuwait City.', slot: 'ms-1980' },
150    { year: '1982', title: 'Farwaniya Showroom', desc: 'Our Farwaniya showroom opened its doors, bringing our brands closer to families across the governorate.', slot: 'ms-1982' },
151    { year: '1999', title: 'New Headquarters in Dawliya Complex', desc: 'We relocated to a new headquarters in the Dawliya Complex, anchoring our operations for the decades ahead.', slot: 'ms-1999' },
152    { year: '2009', title: 'Appliance Showroom — Jahra, Farwaniya & Hawally', desc: 'We launched dedicated appliance showrooms in Jahra, Farwaniya and Hawally, widening access to home and electronic appliances.', slot: 'ms-2009' },
153    { year: '2017', title: 'Yaal Showroom', desc: 'The Yaal showroom opened, adding a modern retail destination to our growing network.', slot: 'ms-2017' },
154    { year: '2020', title: 'New Hawally Department Store', desc: 'We unveiled our new Hawally department store — a flagship retail experience for a new generation of customers.', slot: 'ms-2020' },
155    { year: '2024', title: 'Launch of Aluna', desc: 'We introduced Aluna, our dedicated lingerie and shapewear destination, bringing internationally acclaimed lingerie and shapewear to customers across Kuwait.', slot: 'ms-2024' },
156    { year: '2026', title: 'Union Electronics', desc: 'We brought Union Electronics to life across Physical and Digital Stores, delivering a seamless omni-channel experience for the latest technology and home appliances.', slot: 'ms-2026' }
157  ];
158
159  const socials = [
160    { name: 'Instagram', href: 'https://www.instagram.com/unionelectronics.kw/', icon: sic('<path d="M12 8.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7zm0 2a1.5 1.5 0 110 3 1.5 1.5 0 010-3z"></path><path d="M7 3h10a4 4 0 014 4v10a4 4 0 01-4 4H7a4 4 0 01-4-4V7a4 4 0 014-4zm0 2a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2H7zm10.5 1.5a1 1 0 110 2 1 1 0 010-2z"></path>') },
161    { name: 'Facebook', href: 'https://www.facebook.com/unionelectronicskw', icon: sic('<path d="M14 9h3V5h-3c-2.2 0-4 1.8-4 4v2H7v4h3v6h4v-6h3l1-4h-4V9c0-.6.4-1 1-1z"></path>') },
162    { name: 'LinkedIn', href: 'https://www.linkedin.com/company/union-trading-company-kuwait/', icon: sic('<path d="M6.5 8v10H3.5V8h3zM5 3.5A1.75 1.75 0 105 7 1.75 1.75 0 005 3.5zM20.5 18h-3v-5c0-1.3-.5-2-1.5-2s-1.5.7-1.5 2v5h-3V8h3v1.3c.5-.8 1.4-1.5 2.7-1.5 2 0 3.3 1.3 3.3 4z"></path>') },
163    { name: 'TikTok', href: 'https://www.tiktok.com/@unionelectronics.kw?_r=1&_t=ZS-998zkW4Kypg', icon: sic('<path d="M16 3c.3 2 1.5 3.5 3.5 4v3c-1.4 0-2.6-.4-3.5-1v5.5A5.5 5.5 0 118 9v3.2A2.5 2.5 0 1011 14V3h3z"></path>') },
164    { name: 'YouTube', href: 'https://youtube.com/@unionelectronicskw?si=DtCur73sViKpsxy3', icon: sic('<path d="M23 12s0-3.2-.4-4.7a2.5 2.5 0 00-1.7-1.7C19.3 5.2 12 5.2 12 5.2s-7.3 0-8.9.4a2.5 2.5 0 00-1.7 1.7C1 8.8 1 12 1 12s0 3.2.4 4.7a2.5 2.5 0 001.7 1.7c1.6.4 8.9.4 8.9.4s7.3 0 8.9-.4a2.5 2.5 0 001.7-1.7C23 15.2 23 12 23 12zM9.8 15.3V8.7l5.7 3.3-5.7 3.3z"></path>') }
165  ];
166  const phoneIcon = '<path d="M4 4h4l2 5-2 2a11 11 0 005 5l2-2 5 2v4a2 2 0 01-2 2A16 16 0 014 6a2 2 0 012-2z"></path>';
167  const contacts = [
168    { icon: ic(phoneIcon), main: '+965 2242 3355', sub: 'Head Office Number' },
169    { icon: ic(phoneIcon), main: '+965 184 0011', sub: 'Service Center Hotline' },
170    { icon: ic(phoneIcon), main: '+965 9407 3737', sub: 'WhatsApp' },
171    { icon: ic('<rect x="3" y="5" width="18" height="14" rx="1"></rect><path d="M3 7l9 6 9-6"></path>'), main: '[email protected]', sub: 'Email Us' },
172    { icon: ic('<path d="M12 21s7-6 7-11a7 7 0 10-14 0c0 5 7 11 7 11z"></path><circle cx="12" cy="10" r="2.5"></circle>'), main: 'Union Trading Co.', sub: 'Rehab Al-Salem Street, Al Dawliya Complex, 3rd Floor, Kuwait City.' }
173  ];
174  const footAbout = ['About UTC', 'FAQ', 'Careers', 'Sitemap', 'Privacy Statement'];
175
176  // Offices for the contact panel — each opens the Our Stores explorer; where a
177  // matching store record exists, it deep-links to it via ?store=<index>.
178  const offices = [
179    { name: 'Head Office', addr: ['Dawliah Commercial Center, Block 15', 'Al Qibla, Kuwait City'], phone: '+965 2242 3355', email: '[email protected]', store: 12, img: 'img/Showroom%20Images/HOPrem.webp' },
180    { name: 'Flagship Store', addr: ['Fahad Al-Salem Street, Block 1', 'Kuwait City, Kuwait'], phone: '+965 2242 3355', email: '[email protected]', store: 0, img: 'img/Showroom%20Images/Flagship.webp' },
181    { name: 'Salmiya Store', addr: ['Salem Al Mubarak Street', 'Salmiya, Kuwait'], phone: '+965 9550 1717', email: '[email protected]', store: 1, img: 'img/stores/salmiya-1.webp' }
182  ];
183
184  // Bottom feature strip
185  const features = [
186    { icon: '<circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path>', title: 'Quick Response', sub: 'We aim to respond within 24 business hours.' },
187    { icon: '<path d="M4 14v-2a8 8 0 0116 0v2"></path><rect x="3" y="14" width="4" height="6" rx="1.5"></rect><rect x="17" y="14" width="4" height="6" rx="1.5"></rect>', title: 'Dedicated Support', sub: 'Our team is here to assist you at every step.' },
188    { icon: '<path d="M9 12l2 2 4-4"></path><path d="M12 3l7 3v5c0 4.5-3 8-7 10-4-2-7-5.5-7-10V6z"></path>', title: 'Partnerships', sub: "Interested in becoming a partner? Let's connect." },
189    { icon: '<circle cx="12" cy="12" r="9"></circle><path d="M3 12h18M12 3a15 15 0 010 18M12 3a15 15 0 000 18"></path>', title: 'Global Standards', sub: 'Delivering international qual
189ity with local commitment.' },
190    { icon: '<path d="M4 19l5-5 3 3 7-8"></path><path d="M16 9h4v4"></path>', title: 'Mutual Growth', sub: 'We grow together to create long-term value.' }
191  ];
192
193  /* ---------- state ---------- */
194  const S = { hero: 0, div: 0, storyImg: 0, partner: 0 };
195
196  /* ---------- static renders ---------- */
197  const el = id => document.getElementById(id);
198
199  // nav (About Us -> About page, Our Divisions -> divisions, Contact -> contact)
200  const NAV_HREF = { 'About Us': 'about/', 'Our Divisions': 'divisions/', 'Our Stores': 'our-stores/', 'Our Channels': 'our-channels/', 'Careers': 'careers/', 'Contact': '#contact' };
201  el('nav').innerHTML = NAV.map(l => NAV_SOON.indexOf(l) !== -1
202    ? '<span title="Coming soon" style="color:rgba(255,255,255,0.32);font-size:15px;font-weight:500;letter-spacing:0.01em;padding-bottom:4px;border-bottom:2px solid transparent;cursor:not-allowed">' + l + '</span>'
203    : '<a href="' + (NAV_HREF[l] || '#') + '" class="hoverline" style="color:#fff;font-size:15px;font-weight:500;letter-spacing:0.01em;padding-bottom:4px;border-bottom:2px solid transparent">' + l + '</a>').join('');
204
205  // brand marquee — the full Electronics brand set, rendered as white silhouettes
206  // via the invert filter. Kept separate from brandNames so the division brand-chip
207  // lookup (logoFor) is unaffected.
208  const marqBrands = [
209    { name: 'B&D', src: 'img/brands-elec/b-d.webp' },
210    { name: 'Triumph', src: 'img/brands-aluna/triumph.webp' },
211    { name: 'Belkin', src: 'img/brands-elec/belkin.webp' },
212    { name: 'Canton', src: 'img/brands-elec/canton.webp' },
213    { name: 'Sloggi', src: 'img/brands-aluna/sloggi.webp' },
214    { name: 'Cecotec', src: 'img/brands-elec/cecotec.webp' },
215    { name: 'Changhong', src: 'img/brands-elec/changhong.webp' },
216    { name: 'Maidenform', src: 'img/brands-aluna/maidenform.webp' },
217    { name: 'Chigo', src: 'img/brands-elec/chigo.webp' },
218    { name: 'Daikin', src: 'img/brands-elec/daikin.webp' },
219    { name: 'Hanes', src: 'img/brands-aluna/hanes-mark.webp', raw: true },
220    { name: 'Fakir', src: 'img/brands-elec/fakir.webp' },
221    { name: 'Galanz', src: 'img/brands-elec/galanz.webp' },
222    { name: 'Bali', src: 'img/brands-aluna/bali.webp' },
223    { name: 'General', src: 'img/brands-elec/general.webp' },
224    { name: 'Glemgas', src: 'img/brands-elec/glemgas.webp' },
225    { name: 'Naomi & Nicole', src: 'img/brands-aluna/naomi-nicole.webp' },
226    { name: 'Havic', src: 'img/brands-elec/havic.webp' },
227    { name: 'Hifuture', src: 'img/brands-elec/hifuture.webp' },
228    { name: 'Carole Hochman', src: 'img/brands-aluna/carole-hochman.webp', pad: '6px 14px' },
229    { name: 'Hitachi', src: 'img/brands-elec/hitachi.webp' },
230    { name: 'Konka', src: 'img/brands-elec/konka.webp' },
231    { name: 'Leg Avenue', src: 'img/brands-aluna/leg-avenue.webp' },
232    { name: 'Morphy', src: 'img/brands-elec/morphy.webp' },
233    { name: 'Naim & Focal', src: 'img/brands-elec/naim-focal.webp', big: true },
234    { name: 'Eileen West', src: 'img/brands-aluna/eileen-west.webp', pad: '6px 10px' },
235    { name: 'Proove', src: 'img/brands-elec/proove.webp' },
236    { name: 'ROWA', src: 'img/brands-elec/rowa.webp' },
237    { name: 'Cupid', src: 'img/brands-aluna/cupid.webp' },
238    { name: 'Startec', src: 'img/brands-elec/startec.webp' },
239    { name: 'TCL', src: 'img/brands-elec/tcl.webp' },
240    { name: 'Mapale', src: 'img/brands-aluna/mapale.webp' },
241    { name: 'Vestel', src: 'img/brands-elec/vestel.webp' },
242    { name: 'Ivory', src: 'img/brands-aluna/ivory.webp', pad: '8px 12px' },
243    { name: 'Westpoint', src: 'img/brands-elec/westpoint.webp' },
244    { name: 'C.Lengerie', src: 'img/brands-aluna/c-lengerie.webp' },
245    { name: 'Yuwell', src: 'img/brands-elec/yuwell.webp' },
246    { name: 'G World', src: 'img/brands-aluna/g-world.webp' }
247  ];
248  const marqueeBrands = marqBrands;
249  const marqHtml = marqueeBrands.map(b => {
250    const filt = b.raw ? 'none' : 'brightness(0) invert(1)';
251    return '<div class="bcell" style="width:200px;height:56px;border-right:1px solid rgba(255,255,255,0.16);padding:' + (b.pad || (b.big ? '5px 24px' : '13px 24px')) + ';opacity:0.82;transition:opacity .35s ease">'
252      + '<div class="bimg" role="img" aria-label="' + b.name + '" style="width:100%;height:100%;filter:' + filt + ';background-image:url(&quot;' + b.src + '&quot;);background-size:contain;background-repeat:no-repeat;background-position:center;transition:transform .35s ease"></div></div>';
253  }).join('');
254  const marqsets = document.querySelectorAll('.marqset');
255  marqsets.forEach(m => m.innerHTML = marqHtml);
256  // JS-driven marquee so the first brand starts centred, then scrolls
257  (function () {
258    const row = document.querySelector('.marqrow');
259    if (!row || !marqsets.length) return;
260    row.style.animation = 'none';
261    let W = 0, pos0 = 0, pos = 0, paused = false;
262    function measure() { W = marqsets[0].getBoundingClientRect().width; pos0 = window.innerWidth / 2 - 100 - W; pos = pos0; }
263    measure();
264    row.addEventListener('mouseenter', () => paused = true);
265    row.addEventListener('mouseleave', () => paused = false);
266    window.addEventListener('resize', measure);
267    let last = 0;
268    const PXPS = 30; // px/sec — matched to the Aluna inner-page brand marquee
269    function frame(t) {
270      if (!last) last = t; const dt = t - last; last = t;
271      if (!paused && W) { pos -= dt * PXPS / 1000; if (pos <= pos0 - W) pos += W; row.style.transform = 'translateX(' + pos.toFixed(1) + 'px)'; }
272      requestAnimationFrame(frame);
273    }
274    requestAnimationFrame(frame);
275  })();
276
277  // socials (footer), footAbout
278  el('socials2').innerHTML = socials.map(s => '<a href="' + s.href + '" target="_blank" rel="noopener" aria-label="' + s.name + '" style="width:34px;height:34px;border:1px solid #33312d;display:flex;align-items:center;justify-content:center;color:#cfcbc0">' + s.icon + '</a>').join('');
279
280  // office rows — open the Our Stores explorer (deep-linked to the store where one exists)
281  const officeIcon = (p) => '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" style="flex-shrink:0">' + p + '</svg>';
282  const officesEl = el('offices');
283  if (officesEl) {
284    officesEl.innerHTML = offices.map((o, i) => {
285      const href = o.store != null ? 'our-stores/?store=' + o.store : 'our-stores/';
286      const divider = i < offices.length - 1 ? 'border-bottom:1px solid #ece8de;' : '';
287      return '<a class="office-card" href="' + href + '" style="display:flex;gap:14px;align-items:center;padding:8px 10px;text-decoration:none;color:inherit;border-radius:12px;' + divider + 'transition:background .25s">'
288        + '<span style="width:56px;height:52px;flex-shrink:0;border-radius:10px;background:#eceae2 url(&quot;' + o.img + '&quot;) center/cover no-repeat"></span>'
289        + '<div style="min-width:0;flex:1">'
290        + '<div style="font-family:\'Archivo\';font-weight:700;font-size:15px;color:#161513;margin-bottom:3px">' + o.name + '</div>'
291        + '<div style="font-size:12px;color:#645f57;line-height:1.4;margin-bottom:5px">' + o.addr.join('<br>') + '</div>'
292        + '<div style="display:flex;flex-wrap:wrap;gap:6px 18px">'
293        + '<span style="display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:#5c5951">' + officeIcon('<path d="M4 4h4l2 5-2 2a11 11 0 005 5l2-2 5 2v4a2 2 0 01-2 2A16 16 0 014 6a2 2 0 012-2z"></path>') + o.phone + '</span>'
294        + '</div>'
295        + '</div>'
296        + '<span class="office-arrow" style="width:38px;height:38px;flex-shrink:0;border-radius:50%;border:1px solid #ddd8cc;display:flex;align-items:center;justify-content:center;color:#161513;align-self:center;transition:background .25s,color .25s,border-color .25s"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"></path></svg></span>'
297        + '</a>';
298    }).join('');
299
300    // mobile only: pagination dots for the offices swipe-carousel (hidden on desktop via inline display:none)
301    if (!document.querySelector('.office-dots')) {
302      const dw = document.createElement('div');
303      dw.className = 'office-dots';
304      dw.style.cssText = 'display:none;justify-content:center;gap:7px;margin-top:12px';
305      dw.innerHTML = offices.map((o, i) => '<button class="tapx" data-od="' + i + '" aria-label="' + o.name + '" style="width:8px;height:8px;border-radius:50%;border:none;padding:0;cursor:pointer;background:' + (i === 0 ? '#a97f43' : 'rgba(60,48,30,0.22)') + ';transition:width .25s ease,border-radius .25s ease,background .25s ease"></button>').join('');
306      officesEl.parentNode.insertBefore(dw, officesEl.nextSibling);
307      const dots = Array.prototype.slice.call(dw.children);
308      const items = Array.prototype.slice.call(officesEl.children);
309      const activeIdx = () => { const mid = officesEl.scrollLeft + officesEl.clientWidth / 2; let best = 0, bd = Infinity; items.forEach((c, i) => { const cen = c.offsetLeft + c.offsetWidth / 2, d = Math.abs(cen - mid); if (d < bd) { bd = d; best = i; } }); return best; };
310      const sync = () => { const a = activeIdx(); dots.forEach((d, i) => { const on = i === a; d.style.background = on ? '#a97f43' : 'rgba(60,48,30,0.22)'; d.style.width = on ? '20px' : '8px'; d.style.borderRadius = on ? '4px' : '50%'; }); };
311      let oraf = 0;
312      officesEl.addEventListener('scroll', () => { cancelAnimationFrame(oraf); oraf = requestAnimationFrame(sync); }, { passive: true });
313      dots.forEach((d, i) => d.addEventListener('click', () => { const c = items[i]; officesEl.scrollTo({ left: c.offsetLeft - (officesEl.clientWidth - c.offsetWidth) / 2, behavior: 'smooth' }); }));
314      window.addEventListener('resize', sync);
315      setTimeout(sync, 200);
316    }
317  }
318
319  // feature strip — minimal gold-accent icon + title & description, with slim centered dividers
320  const featEl = document.querySelector('.feat-grid');
321  if (featEl) {
322    featEl.innerHTML = features.map((f, i) => {
323      const div = i > 0 ? 'background:linear-gradient(#e6e0d3,#e6e0d3) left center / 1px 54% no-repeat;' : '';
324      return '<div style="display:flex;gap:13px;align-items:center;padding:2px clamp(14px,1.5vw,22px);' + div + '">'
325        + '<div style="width:40px;height:40px;flex-shrink:0;border-radius:50%;background:#f4eee2;display:flex;align-items:center;justify-content:center;color:#a97f43"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">' + f.icon + '</svg></div>'
326        + '<div style="min-width:0"><div style="font-family:\'Archivo\';font-weight:700;font-size:14px;letter-spacing:-0.01em;color:#161513;margin-bottom:4px">' + f.title + '</div>'
327        + '<div style="font-size:12px;color:#635e55;line-height:1.45">' + f.sub + '</div></div>'
328        + '</div>';
329    }).join('');
330
331    // mobile only: pagination dots for the feature swipe-carousel (hidden on desktop via inline display:none)
332    if (!document.querySelector('.feat-dots')) {
333      const dw = document.createElement('div');
334      dw.className = 'feat-dots';
335      dw.style.cssText = 'display:none;justify-content:center;gap:7px;margin-top:14px';
336      dw.innerHTML = features.map((f, i) => '<button class="tapx" data-fd="' + i + '" aria-label="' + f.title + '" style="width:8px;height:8px;border-radius:50%;border:none;padding:0;cursor:pointer;background:' + (i === 0 ? '#a97f43' : 'rgba(60,48,30,0.22)') + ';transition:width .25s ease,border-radius .25s ease,background .25s ease"></button>').join('');
337      featEl.parentNode.insertBefore(dw, featEl.nextSibling);
338      const dots = Array.prototype.slice.call(dw.children);
339      const items = Array.prototype.slice.call(featEl.children);
340      const activeIdx = () => { const mid = featEl.scrollLeft + featEl.clientWidth / 2; let best = 0, bd = Infinity; items.forEach((c, i) => { const cen = c.offsetLeft + c.offsetWidth / 2, d = Math.abs(cen - mid); if (d < bd) { bd = d; best = i; } }); return best; };
341      const sync = () => { const a = activeIdx(); dots.forEach((d, i) => { const on = i === a; d.style.background = on ? '#a97f43' : 'rgba(60,48,30,0.22)'; d.style.width = on ? '20px' : '8px'; d.style.borderRadius = on ? '4px' : '50%'; }); };
342      let fraf = 0;
343      featEl.addEventListener('scroll', () => { cancelAnimationFrame(fraf); fraf = requestAnimationFrame(sync); }, { passive: true });
344      dots.forEach((d, i) => d.addEventListener('click', () => { const c = items[i]; featEl.scrollTo({ left: c.offsetLeft - (featEl.clientWidth - c.offsetWidth) / 2, behavior: 'smooth' }); }));
345      window.addEventListener('resize', sync);
346      setTimeout(sync, 200);
347    }
348  }
349
350  const footHref = { 'Careers': 'careers/', 'About UTC': 'about/', 'Sitemap': 'sitemap/', 'Privacy Statement': 'privacy-policy/', 'FAQ': 'faq/' };
351  const footSoon = ['Careers'];   // careers is an unfinished placeholder — keep it un-linked for now
352  el('footAbout').innerHTML = footAbout.map(l => footSoon.indexOf(l) !== -1
353    ? '<span title="Coming soon" style="color:#5c5850;font-size:14px;cursor:default;pointer-events:none">
353' + l + ' <span style="font-size:10px;letter-spacing:.1em;opacity:.8">&middot; SOON</span></span>'
354    : '<a href="' + (footHref[l] || '#') + '" style="color:#8f8c85;font-size:14px">' + l + '</a>').join('');
355
356  // hero slides + dots
357  el('heroSlides').innerHTML = '';
358  heroImgs.forEach((img, i) => {
359    const layer = document.createElement('div');
360    layer.style.cssText = 'position:absolute;inset:0;transition:opacity 800ms ease;opacity:' + (i === 0 ? 1 : 0) + ';pointer-events:none';
361    layer.appendChild(slotEl('hero-' + i, img, 'cover'));
362    layer.dataset.h = i;
363    el('heroSlides').appendChild(layer);
364  });
365  el('heroDots').innerHTML = heroImgs.map((_, i) => '<button class="tapx" data-hd="' + i + '" aria-label="slide" style="width:9px;height:9px;border-radius:50%;border:none;cursor:pointer;padding:0;background:' + (i === 0 ? '#141311' : 'rgba(255,255,255,0.55)') + '"></button>').join('');
366  el('heroDots').querySelectorAll('button').forEach(b => b.onclick = () => setHero(+b.dataset.hd));
367  el('heroMuted').textContent = ('0' + heroImgs.length).slice(-2);  // total-slides counter, auto-matches heroImgs
368
369  // division image layers + tabs
370  el('divLayers').innerHTML = '';
371  divData.forEach((d, i) => {
372    const layer = document.createElement('div');
373    layer.style.cssText = 'position:absolute;inset:0;transition:opacity 500ms ease;opacity:' + (i === 0 ? 1 : 0) + ';z-index:' + (i === 0 ? 2 : 1) + ';pointer-events:none';
374    layer.appendChild(slotEl(d.slot, 'img/' + d.slot + '.webp', 'cover'));
375    el('divLayers').appendChild(layer);
376  });
377  function tabHtml() {
378    return divData.map((d, i) => {
379      const active = i === S.div;
380      const divider = (i === divData.length - 1 || i === S.div || i === S.div - 1) ? '1px solid transparent' : '1px solid #2c2a26';
381      const inner = d.logo
382        ? '<img loading="lazy" decoding="async" src="' + d.logo + '" alt="' + d.name + '" style="width:' + (d.tabW || '100px') + ';height:36px;object-fit:contain;filter:brightness(0) invert(1);display:block">'
383        : '<span style="width:26px;height:26px;display:flex;align-items:center;justify-content:center">' + ic(d.icon) + '</span><span style="font-size:12px;font-weight:700;text-align:center;line-height:1.2">' + d.name + '</span>';
384      return '<button data-dt="' + i + '" class="divtab' + (active ? ' active' : '') + '" style="cursor:pointer;background:' + (active ? '#262a2e' : 'transparent') + ';border:none;border-right:' + divider + ';color:' + (active ? '#ffffff' : '#b4b8bc') + ';padding:11px 14px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;flex:1 1 0;min-width:118px;min-height:56px;border-radius:14px;position:relative;transition:background 250ms ease">' + inner + '</button>';
385    }).join('');
386  }
387  el('divTabs').innerHTML = tabHtml();
388  el('divTabs').querySelectorAll('button').forEach(b => b.onclick = () => setDiv(+b.dataset.dt));
389  el('divPrev').onclick = () => setDiv((S.div - 1 + divData.length) % divData.length);
390  el('divNext').onclick = () => setDiv((S.div + 1) % divData.length);
391  // Swipe left/right on the divisions card to navigate (touch devices)
392  (function () {
393    const card = document.querySelector('#divPanel .divcard') || el('divPanel');
394    if (!card) return;
395    let x0 = 0, y0 = 0, t0 = 0, lx = 0, ly = 0;
396    card.addEventListener('touchstart', e => { const t = e.changedTouches[0]; if (!t) return; x0 = lx = t.clientX; y0 = ly = t.clientY; t0 = Date.now(); }, { passive: true });
397    card.addEventListener('touchmove', e => { const t = e.changedTouches[0]; if (t) { lx = t.clientX; ly = t.clientY; } }, { passive: true });
398    card.addEventListener('touchend', e => {
399      const t = e.changedTouches[0];
400      const ex = t ? t.clientX : lx, ey = t ? t.clientY : ly;
401      const dx = ex - x0, dy = ey - y0;
402      if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy) * 1.4 && Date.now() - t0 < 700) {
403        setDiv((S.div + (dx < 0 ? 1 : -1) + divData.length) % divData.length);
404      }
405    }, { passive: true });
406  })();
407  // Re-fit the description when crossing the mobile/desktop breakpoint
408  window.addEventListener('resize', () => { const d = divData[S.div]; el('divDesc').innerHTML = d.descHtml || d.desc; clampDesc(); });
409
410  // story photo layers (storefront + 2 extras) — built into the desktop frame and,
411  // when present, the mobile frame so both show the same angled slideshow.
412  const STORY_SRC = [{ id: 'story-storefront', src: 'img/story-storefront.webp' }, { id: 'story-x0', src: storyExtra[0] }, { id: 'story-x1', src: storyExtra[1] }];
413  function buildStoryLayers(container) {
414    const layers = [];
415    STORY_SRC.forEach((o, i) => {
416      const layer = document.createElement('div');
417      layer.style.cssText = 'position:absolute;inset:0;opacity:' + (i === 0 ? 1 : 0) + ';transition:opacity 1.1s ease';
418      layer.appendChild(slotEl(o.id, o.src, 'cover'));
419      container.appendChild(layer);
420      layers.push(layer);
421    });
422    return layers;
423  }
424  const storyLayers = buildStoryLayers(el('storyPhotos'));
425  const storyPhotosMobileEl = document.getElementById('storyPhotosMobile');
426  const storyLayersMobile = storyPhotosMobileEl ? buildStoryLayers(storyPhotosMobileEl) : [];
427
428  // story stats (desktop cqw + mobile)
429  el('storyStats').innerHTML = storyStats.map(s => '<div style="display:flex;gap:1.2cqw;align-items:center"><div style="width:4cqw;height:4cqw;min-width:42px;min-height:42px;border-radius:50%;border:1px solid rgba(255,255,255,0.22);display:flex;align-items:center;justify-content:center;color:#cfcbc2;flex-shrink:0">' + s.icon + '</div><div><div data-kpi="' + s.n + '" style="font-family:\'Archivo\';font-weight:800;font-size:3.6cqw;line-height:1;color:#fff">' + s.n + '</div><div style="font-size:0.95cqw;letter-spacing:0.08em;color:#cbc7be;font-weight:700;margin:0.55cqw 0 0.3cqw">' + s.l + '</div><div style="font-size:0.8cqw;line-height:1.4;color:#8a867e">' + s.sub + '</div></div></div>').join('');
430  el('storyStatsMobile').innerHTML = storyStats.map(s => '<div><div style="font-family:\'Archivo\';font-weight:800;font-size:34px;color:#fff">' + s.n + '</div><div style="font-size:12px;letter-spacing:0.06em;color:#cbc7be;font-weight:700;margin:6px 0 3px">' + s.l + '</div><div style="font-size:12px;color:#8a867e">' + s.sub + '</div></div>').join('');
431
432  // partner rail dots, image layers, cells
433  function renderPartnerStatic() {
434    el('partnerRail').innerHTML = plist.map((p, i) => {
435      const a = i === activePartner();
436      return '<div data-pr="' + i + '" style="display:flex;align-items:center;gap:12px;cursor:pointer"><span style="font-size:11px;font-weight:700;letter-spacing:0.1em;width:18px;color:' + (a ? '#161513' : '#b3afa6') + '">0' + (i + 1) + '</span><span style="width:9px;height:9px;border-radius:50%;background:' + (a ? '#161513' : 'transparent') + ';border:1px solid ' + (a ? '#161513' : '#c3bfb5') + ';box-sizing:border-box"></span></div>';
437    }).join('');
438    el('partnerRail').querySelectorAll('[data-pr]').forEach(d => d.onclick = () => setPartner(+d.dataset.pr));
439  }
440  el('partnerImages').innerHTML = '';
441  plist.forEach((p, i) => {
442    const layer = document.createElement('div');
443    layer.style.cssText = 'position:absolute;inset:0;opacity:' + (i === 0 ? 1 : 0) + ';transition:opacity .8s ease;pointer-events:none;z-index:' + (i === 0 ? 2 : 1);
444    if (!p.noAssets) layer.appendChild(slotEl('pimg-' + i, 'img/pimg-' + i + '.webp', 'cover'));  // no brand image yet -> clean dark panel
445    el('partnerImages').appendChild(layer);
446  });
447  function renderPartnerCells() {
448    const a = activePartner();
449    let h = plist.map((p, i) => {
450      const on = i === a;
451      const bar = p.src
452        ? '<div class="slot pbar" data-slot="pbar-' + i + '" data-fit="contain" data-src="' + p.src + '" style="height:clamp(26px,2.4vw,34px);width:100%;position:relative;filter:' + (on ? 'invert(1) brightness(2)' : 'none') + '"></div>'
453        : '<div style="height:clamp(26px,2.4vw,34px);width:100%;display:flex;align-items:center;justify-content:center;filter:' + (on ? 'invert(1) brightness(2)' : 'none') + '"><span style="font-family:\'Archivo\',sans-serif;font-weight:800;font-size:clamp(13px,1.1vw,16px);color:#161513;white-space:nowrap">' + p.name + '</span></div>';
454      return '<div data-pc="' + i + '" style="flex:1;min-width:0;padding:clamp(16px,1.8vw,26px) clamp(10px,1.5vw,22px);background:' + (on ? '#141414' : '#ffffff') + ';border-right:1px solid #ededed;cursor:pointer;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px">'
455        + '<span style="position:absolute;top:10px;left:14px;font-size:10px;font-weight:700;letter-spacing:0.1em;color:' + (on ? '#7a7a7a' : '#b3afa6') + '">0' + (i + 1) + '</span>'
456        + bar
457        + '<span style="font-size:10px;letter-spacing:0.12em;font-weight:600;color:' + (on ? '#8a8a8a' : '#67645d') + '">SINCE ' + p.since + '</span></div>';
458    }).join('');
459    h += '<div class="pc-more-cell" style="flex:0.8;min-width:0;padding:clamp(16px,1.8vw,26px) clamp(12px,1.6vw,24px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px"><span class="pc-more" style="font-size:11px;letter-spacing:0.14em;font-weight:700;color:#6d6a62;text-align:center;line-height:1.5">AND MORE<br>PARTNERS</span><span style="width:34px;height:1px;background:#c8c4ba"></span></div>';
460    el('partnerCells').innerHTML = h;
461    el('partnerCells').querySelectorAll('.pbar').forEach(sl => fillSlot(sl, sl.dataset.slot, sl.dataset.src, 'contain'));
462    el('partnerCells').querySelectorAll('[data-pc]').forEach(d => d.onclick = () => setPartner(+d.dataset.pc));
463  }
464  el('partnerLogo').style.width = '100%';
465
466  // milestones panels + mobile
467  el('msPanels').innerHTML = milestones.map(m =>
468    '<div data-ms-panel style="position:absolute;inset:0;overflow:hidden;border-radius:14px;background:#151515;backface-visibility:hidden;transform-origin:center center;will-change:transform,opacity;box-shadow:0 28px 70px -26px rgba(0,0,0,.85)">'
469    + '<div class="slot" data-slot="' + m.slot + '"><img alt="" src="img/' + m.slot + '.webp"></div>'
470    + '<div style="position:absolute;inset:0;background:linear-gradient(90deg,rgba(10,10,10,.92) 0%,rgba(10,10,10,.72) 30%,rgba(10,10,10,.25) 62%,rgba(10,10,10,0) 85%)"></div>'
471    + '<div style="position:absolute;left:0;top:0;bottom:0;width:min(52%,420px);display:flex;flex-direction:column;justify-content:center;padding:clamp(28px,4vw,54px)">'
472    + '<div style="font-family:\'Archivo\';font-weight:800;font-size:clamp(40px,4.6vw,68px);line-height:0.9;color:#fff">' + m.year + '</div>'
473    + '<h3 style="font-family:\'Archivo\';font-weight:700;font-size:clamp(18px,1.9vw,26px);line-height:1.15;margin:clamp(12px,1.6vh,18px) 0 14px;color:#fff">' + m.title + '</h3>'
474    + '<div style="width:36px;height:2px;background:#7a7a7a;margin-bottom:14px"></div>'
475    + '<p style="color:#b5b5b5;
475font-size:clamp(13px,1.05vw,15px);line-height:1.6;margin:0;max-width:300px">' + m.desc + '</p></div>'
476    + '<div data-ms-dim style="position:absolute;inset:0;background:#0a0a0a;opacity:0.5;transition:opacity .45s ease;pointer-events:none"></div>'
477    + '<div data-ms-ring style="position:absolute;inset:0;border:1px solid rgba(255,255,255,.09);border-radius:14px;opacity:0;transition:opacity .45s ease;pointer-events:none"></div></div>'
478  ).join('');
479  el('msMobile').innerHTML = milestones.map(m =>
480    '<div style="position:relative;border-radius:14px;overflow:hidden;margin-bottom:16px;min-height:200px;background:#151515">'
481    + '<div class="slot" data-slot="' + m.slot + '-m"><img alt="" src="img/' + m.slot + '.webp"></div>'
482    + '<div style="position:absolute;inset:0;background:linear-gradient(90deg,rgba(10,10,10,.92),rgba(10,10,10,.4))"></div>'
483    + '<div style="position:relative;padding:24px"><div style="font-family:\'Archivo\';font-weight:800;font-size:42px;color:#fff">' + m.year + '</div>'
484    + '<h3 style="font-family:\'Archivo\';font-weight:700;font-size:20px;margin:10px 0 10px;color:#fff">' + m.title + '</h3>'
485    + '<div style="width:36px;height:2px;background:#7a7a7a;margin-bottom:12px"></div>'
486    + '<p style="color:#b5b5b5;font-size:14px;line-height:1.6;margin:0;max-width:340px">' + m.desc + '</p></div></div>'
487  ).join('');
488
489  // Mobile milestones: horizontal swipe carousel with dots (desktop uses the 3D drum)
490  (function () {
491    const mob = el('msMobile'), dw = el('msDots');
492    if (!mob || !dw) return;
493    const N = mob.children.length;
494    dw.innerHTML = milestones.map((m, i) =>
495      '<button class="tapx" data-msdot="' + i + '" aria-label="' + m.year + '" title="' + m.year + '" style="width:8px;height:8px;border-radius:50%;border:none;padding:0;cursor:pointer;background:rgba(255,255,255,0.3);transition:width .25s ease,border-radius .25s ease,background .25s ease"></button>'
496    ).join('');
497    const dots = Array.prototype.slice.call(dw.children);
498    const activeIdx = () => {
499      const cs = mob.children, mid = mob.scrollLeft + mob.clientWidth / 2;
500      let best = 0, bd = Infinity;
501      for (let i = 0; i < cs.length; i++) { const c = cs[i], center = c.offsetLeft + c.offsetWidth / 2, d = Math.abs(center - mid); if (d < bd) { bd = d; best = i; } }
502      return best;
503    };
504    const sync = () => { const a = activeIdx(); dots.forEach((d, i) => { const on = i === a; d.style.background = on ? '#fff' : 'rgba(255,255,255,0.3)'; d.style.width = on ? '22px' : '8px'; d.style.borderRadius = on ? '5px' : '50%'; }); };
505    let raf = 0;
506    mob.addEventListener('scroll', () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(sync); }, { passive: true });
507    dots.forEach((d, i) => d.addEventListener('click', () => { const c = mob.children[i]; mob.scrollTo({ left: c.offsetLeft - (mob.clientWidth - c.offsetWidth) / 2, behavior: 'smooth' }); }));
508    window.addEventListener('resize', sync);
509    setTimeout(sync, 200);
510
511    /* Auto-advance the mobile carousel through all milestones. The timer is CLEARED
512       entirely whenever the carousel is off-screen or the tab is hidden (no work in
513       the background), and briefly held after any manual swipe/tap so it never fights
514       the user. On desktop the carousel is display:none (offsetParent === null), so
515       this stays dormant and the 3D drum takes over. */
516    const AUTO_MS = 3600, RESUME_MS = 5000;
517    let autoTimer = 0, pausedUntil = 0, onScreen = false;
518    const shown = () => mob.offsetParent !== null;               // true only in the mobile layout
519    const goTo = i => { const c = mob.children[i]; if (c) mob.scrollTo({ left: c.offsetLeft - (mob.clientWidth - c.offsetWidth) / 2, behavior: 'smooth' }); };
520    const beat = () => { if (!onScreen || !shown() || Date.now() < pausedUntil) return; goTo((activeIdx() + 1) % N); };
521    const startAuto = () => { if (!autoTimer && shown()) autoTimer = setInterval(beat, AUTO_MS); };
522    const stopAuto = () => { if (autoTimer) { clearInterval(autoTimer); autoTimer = 0; } };
523    const hold = () => { pausedUntil = Date.now() + RESUME_MS; };
524    ['touchstart', 'pointerdown', 'wheel'].forEach(ev => mob.addEventListener(ev, hold, { passive: true }));
525    dots.forEach(d => d.addEventListener('click', hold));
526    const io = new IntersectionObserver(es => es.forEach(e => {
527      onScreen = e.isIntersecting;
528      if (onScreen) startAuto(); else stopAuto();
529    }), { threshold: 0.2 });
530    io.observe(mob);
531    document.addEventListener('visibilitychange', () => { if (document.hidden) stopAuto(); else if (onScreen) startAuto(); });
532  })();
533
534  /* ---------- hero controller ---------- */
535  function setHero(i) {
536    S.hero = ((i % heroImgs.length) + heroImgs.length) % heroImgs.length;
537    const pal = heroPalette()[S.hero];
538    el('heroText').style.background = pal.heroBg;
539    el('heroKick').style.color = pal.heroKick;
540    el('heroHead').style.color = pal.heroHead;
541    el('heroSub').style.color = pal.heroSub;
542    el('heroCta').style.background = pal.heroCtaBg;
543    el('heroCta').style.color = pal.heroCtaTx;
544    el('heroLabel').textContent = '0' + (S.hero + 1);
545    el('heroLabel').style.color = pal.heroHead;
546    el('heroTrack').style.background = pal.heroTrack;
547    el('heroFill').style.background = pal.heroFill;
548    el('heroFill').style.width = Math.round((S.hero + 1) / heroImgs.length * 100) + '%';
549    el('heroMuted').style.color = pal.heroMuted;
550    el('heroPartner').textContent = heroPartners[S.hero];
551    const lite = heroLabelLight[S.hero] !== false;
552    el('heroPartner').style.color = lite ? '#ffffff' : '#141311';
553    el('heroPartnerKick').style.color = lite ? 'rgba(255,255,255,0.7)' : '#5a5750';
554    el('heroSlides').querySelectorAll('[data-h]').forEach(l => l.style.opacity = (+l.dataset.h === S.hero) ? 1 : 0);
555    el('heroDots').querySelectorAll('button').forEach(b => b.style.background = (+b.dataset.hd === S.hero) ? '#141311' : 'rgba(255,255,255,0.55)');
556  }
557  let heroTimer = setInterval(() => setHero(S.hero + 1), 5000);
558
559  /* ---------- divisions controller ---------- */
560  // Mobile-only: clamp the division description to 3 lines, trimming at a word
561  // boundary and stripping trailing punctuation so the ellipsis has no comma.
562  function clampDesc() {
563    const el = document.getElementById('divDesc');
564    if (!el || window.innerWidth > 900) return;
565    const full = (divData[S.div] && divData[S.div].desc) || el.textContent || '';
566    el.textContent = full;
567    const lh = parseFloat(getComputedStyle(el).lineHeight) || 22;
568    const maxH = lh * 3 + 2;
569    if (el.scrollHeight <= maxH) return;
570    const words = full.split(/\s+/);
571    const strip = s => s.replace(/[\s.,;:!?–—-]+$/, '');
572    let lo = 1, hi = words.length, best = 1;
573    while (lo <= hi) {
574      const mid = (lo + hi) >> 1;
575      el.textContent = strip(words.slice(0, mid).join(' ')) + '…';
576      if (el.scrollHeight <= maxH) { best = mid; lo = mid + 1; } else { hi = mid - 1; }
577    }
578    el.textContent = strip(words.slice(0, best).join(' ')) + '…';
579  }
580  function setDiv(i) {
581    S.div = ((i % divData.length) + divData.length) % divData.length;
582    const d = divData[S.div];
583    el('divTagline').textContent = d.tagline;
584    el('divCounter').textContent = '0' + (S.div + 1);
585    el('divIcon').innerHTML = ic(d.icon);
586    if (d.cardLogo) {
587      el('divName').innerHTML = '<img loading="lazy" decoding="async" src="' + d.cardLogo + '" alt="' + d.name + '" style="height:' + (d.cardLogoH || 'clamp(32px,4vw,50px)') + ';width:auto;max-width:100%;object-fit:contain;object-position:left top;display:block' + (d.cardLogoDark ? ';filter:brightness(0)' : '') + '">';
588    } else {
589      el('divName').textContent = d.name;
590    }
591    el('divDesc').innerHTML = d.descHtml || d.desc;
592    clampDesc();
593    el('divMgr').textContent = d.mgr;
594    el('divContact').textContent = d.contact;
595    var vm = el('divMore');
596    if (vm) {
597      if (d.soon) {
598        vm.textContent = 'Coming soon';
599        vm.removeAttribute('href');
600        vm.style.color = '#a7a39a'; vm.style.borderColor = '#d8d3c7'; vm.style.background = 'transparent'; vm.style.cursor = 'default'; vm.style.pointerEvents = 'none';
601      } else {
602        vm.textContent = 'View More';
603        vm.setAttribute('href', d.href || '#contact');
604        vm.style.color = '#161513'; vm.style.borderColor = '#161513'; vm.style.background = ''; vm.style.cursor = ''; vm.style.pointerEvents = '';
605      }
606    }
607    // featured brands
608    const featLabel = el('divBrandsLabel');
609    if (featLabel) featLabel.textContent = d.services ? 'FEATURED 
609SERVICES' : 'FEATURED BRANDS';
610    // services division carries a longer description + a list — let its description size to content so the list/pill fit the fixed card
611    var _dd = el('divDesc'); if (_dd) _dd.style.height = d.services ? 'auto' : '200px';
612    if (d.services) {
613      if (featLabel) featLabel.style.display = '';
614      var sShown = d.services.slice(0, 2), sMore = d.services.length - sShown.length;
615      el('divBrands').innerHTML = '<div class="div-services" style="display:flex;flex-direction:column;align-items:flex-start;gap:12px;width:100%">' + sShown.map(function (s) { return '<div style="display:flex;gap:11px;align-items:center;font-size:14px;color:#5c594f;line-height:1.4"><span style="width:5px;height:5px;border-radius:50%;background:#161513;flex:none"></span><span>' + s + '</span></div>'; }).join('') + (sMore > 0 ? '<div style="align-self:flex-start;display:inline-flex;align-items:center;border:1px solid #d8d3c7;background:transparent;padding:9px 18px;border-radius:999px;margin-top:4px"><span style="font-family:\'Archivo\';font-weight:700;font-size:12px;letter-spacing:.03em;color:#57544c">+' + sMore + ' more services</span></div>' : '') + '</div>';
616    } else if (d.brandsSoon) {
617      if (featLabel) featLabel.style.display = '';
618      el('divBrands').innerHTML = '<div style="border:1px solid #d8d3c7;background:#fff;height:48px;min-width:120px;display:flex;align-items:center;justify-content:center;padding:0 22px;border-radius:12px"><span style="font-family:\'Archivo\';font-weight:600;font-size:13px;color:#a7a39a">Coming soon</span></div>';
619    } else {
620    const raw = d.brands || [];
621    const named = raw.filter(n => !/^\+/.test(n));
622    const moreEntry = raw.find(n => /^\+/.test(n));
623    const moreNum = moreEntry ? (parseInt(moreEntry.replace(/\D/g, ''), 10) || 0) : 0;
624    const shown = named.slice(0, 3);
625    const moreTotal = (named.length - shown.length) + moreNum;
626    if (featLabel) featLabel.style.display = named.length ? '' : 'none';
627    let bh = shown.map(name => {
628      const src = logoFor(name);
629      if (src) return '<div style="background:#0d0d0d;height:48px;width:126px;display:flex;align-items:center;justify-content:center;padding:0 20px;border-radius:12px"><div role="img" aria-label="' + name + '" style="width:100%;height:26px;filter:brightness(0) invert(1);background-image:url(&quot;' + src + '&quot;);background-size:contain;background-repeat:no-repeat;background-position:center"></div></div>';
630      return '<div style="background:#0d0d0d;height:48px;min-width:78px;display:flex;align-items:center;justify-content:center;padding:0 22px;border-radius:12px"><span style="font-family:\'Archivo\';font-weight:700;font-size:13px;color:#fff">' + name + '</span></div>';
631    }).join('');
632    if (moreTotal > 0) bh += '<div style="border:1px solid #cfd1d4;background:#fff;height:48px;min-width:72px;display:flex;align-items:center;justify-content:center;padding:0 18px;border-radius:12px"><span style="font-family:\'Archivo\';font-weight:700;font-size:13px;color:#57544c">+' + moreTotal + ' more</span></div>';
633    el('divBrands').innerHTML = bh;
634    }
635    // image layers
636    Array.from(el('divLayers').children).forEach((l, i) => { l.style.opacity = i === S.div ? 1 : 0; l.style.zIndex = i === S.div ? 2 : 1; });
637    // tabs
638    el('divTabs').innerHTML = tabHtml();
639    el('divTabs').querySelectorAll('button').forEach(b => b.onclick = () => setDiv(+b.dataset.dt));
640  }
641  let divTimer = setInterval(() => setDiv(S.div + 1), 7000);
642
643  /* ---------- story image cycle ---------- */
644  setInterval(() => {
645    S.storyImg++;
646    const active = S.storyImg % storyLayers.length;
647    storyLayers.forEach((l, i) => l.style.opacity = i === active ? 1 : 0);
648    storyLayersMobile.forEach((l, i) => l.style.opacity = i === active ? 1 : 0);
649  }, 4000);
650
651  /* ---------- partners controller ---------- */
652  function activePartner() { const n = plist.length; return ((S.partner % n) + n) % n; }
653  function setPartner(i) {
654    S.partner = i;
655    const a = activePartner();
656    const p = plist[a];
657    if (p.src) { fillSlot(el('partnerLogo'), 'plogo-' + a, p.src, 'contain'); }
658    else { el('partnerLogo').innerHTML = '<span style="font-family:\'Archivo\',sans-serif;font-weight:800;font-size:clamp(24px,2.4vw,34px);letter-spacing:.01em;color:#161513;white-space:nowrap">' + p.name + '</span>'; }
659    el('partnerLogo').style.width = '100%';
660    el('partnerQuote').textContent = p.quote;
661    // "View more" appears only when the quote is actually clamped/overflowing
662    var _more = el('partnerMore');
663    if (_more) requestAnimationFrame(function () { var q = el('partnerQuote'); _more.style.display = (q.scrollHeight - q.clientHeight > 2) ? 'inline-flex' : 'none'; });
664    var _avWrap = el('partnerAvatar').parentElement;
665    if (p.noAssets) { if (_avWrap) _avWrap.style.display = 'none'; }
666    else { if (_avWrap) _avWrap.style.display = ''; fillSlot(el('partnerAvatar'), 'pavatar-' + a, 'img/pavatar-' + a + '.webp', 'cover'); }
667    el('partnerPerson').textContent = p.person;
668    el('partnerRole').textContent = p.role;
669    el('partnerYears').textContent = p.years;
670    el('partnerSince').textContent = p.since;
671    Array.from(el('partnerImages').children).forEach((l, i) => { l.style.opacity = i === a ? 1 : 0; l.style.zIndex = i === a ? 2 : 1; });
672    renderPartnerStatic();
673    renderPartnerCells();
674  }
675  let partnerTimer = setInterval(() => setPartner(S.partner + 1), 6000);
676
677  // full-testimonial popup (opened by the card's "View more")
678  function fillPartnerModal() {
679    const p = plist[activePartner()];
680    const lg = el('pmLogo'), nm = el('pmName');
681    if (lg && nm) {
682      if (p.src) { nm.style.display = 'none'; lg.style.display = ''; lg.innerHTML = ''; fillSlot(lg, 'pmlogo', p.src, 'contain'); }
683      else { lg.style.display = 'none'; nm.style.display = ''; nm.textContent = p.name; }
684    }
685    if (el('pmQuote')) el('pmQuote').textContent = p.quote;
686    if (el('pmPerson')) el('pmPerson').textContent = p.person;
687    if (el('pmRole')) el('pmRole').textContent = p.role;
688    if (el('pmYears')) el('pmYears').textContent = p.years;
689  }
690  function openPartnerModal() { const m = el('partnerModal'); if (!m) return; fillPartnerModal(); m.classList.add('open'); clearInterval(partnerTimer); }
691  function closePartnerModal() { const m = el('partnerModal'); if (!m) return; m.classList.remove('open'); clearInterval(partnerTimer); partnerTimer = setInterval(() => setPartner(S.partner + 1), 6000); }
692  if (el('partnerMore')) el('partnerMore').addEventListener('click', openPartnerModal);
693  if (el('partnerModal')) el('partnerModal').addEventListener('click', (e) => { if (e.target.closest('[data-pmclose]')) closePartnerModal(); });
694  document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePartnerModal(); });
695
696  /* ---------- contact form ---------- */
697  // Static site: route the message via mailto. The "I want to…" dropdown decides
698  // the recipient — Partnerships and General Inquiry go to two different inboxes
699  // (each <option> carries its target address in data-mail).
700  // Partnership selection reveals extra fields (type, organisation, other info)
701  // and auto-fills the Subject with "Partnership Inquiry".
702  const cfIntent = el('cfIntent'), cfPart = el('cfPartnership');
703  function syncPartnership() {
704    const on = cfIntent && cfIntent.value === 'partnership';
705    if (cfPart) cfPart.style.display = on ? 'block' : 'none';
706    const subj = el('cfSubject');
707    if (subj) {
708      if (on) { if (!subj.value.trim()) subj.value = 'Partnership Inquiry'; }
709      else if (subj.value.trim() === 'Partnership Inquiry') { subj.value = ''; }
710    }
711  }
712  if (cfIntent) { cfIntent.addEventListener('change', syncPartnership); syncPartnership(); }
713
714  // Preselect "Partnership Opportunity" (and optionally a division) — used by the
715  // inner-page "Partner with us" CTAs and the homepage partner button.
716  function preselectPartnership(division) {
717    if (cfIntent) { cfIntent.value = 'partnership'; syncPartnership(); }
718    const dv = el('cfDivision');
719    if (dv && division) dv.value = division;
720  }
721  // Deep-link from other pages: index.html?intent=partnership&division=<slug>#contact
722  (function () {
723    const p = new URLSearchParams(location.search || '');
724    const intent = p.get('intent'), division = p.get('division');
725    if (intent === 'partnership' || division) {
726      preselectPartnership(division || '');
727      // scroll to the form itself (not the section top) so the preselected dropdowns are visible.
728      // Recompute the absolute position each time — images above load late and shift the layout.
729      const target = el('contactForm') || el('contact');
730      if (target) {
731        const doScroll = function () {
732          const y = target.getBoundingClientRect().top + window.pageYOffset - 90; // clear sticky header
733          window.scrollTo({ top: y, behavior: 'smooth' });
734        };
735        setTimeout(doScroll, 200);
736        window.addEventListener('load', function () { setTimeout(doScroll, 200); });
737      }
738    }
739  })();
740  // Homepage's own "Want to partner with us?" button — preselect without a reload
741  const homePartnerBtn = document.querySelector('a.h-shop[href="#contact"]');
742  if (homePartnerBtn) homePartnerBtn.addEventListener('click', function () { preselectPartnership(''); });
743
744  // Paste your free Web3Forms access key here to have the WEBSITE send submissions
745  // straight to your inbox (get one in 30s at https://web3forms.com — enter your email,
746  // they mail you a key). While this is empty, the form falls back to opening the
747  // visitor's email app (mailto) so nothing breaks.
748  const WEB3FORMS_KEY = '';
749
750  // ── Contact-form routing ─────────────────────────────────────────────────
751  // All enquiries are SENT FROM one central mailbox (the EmailJS-connected account),
752  // and delivered TO the inbox chosen by the enquiry type — and, for partnerships,
753  // by the selected division. Multiple recipients = comma-separated (EmailJS To + mailto both accept it).
754  // PRODUCTION addresses (set 2026-08-30).
755  const MAIL_ROUTES = {
756    general: '[email protected]',                  // General Inquiry
757    support: '[email protected],[email protected],[email protected],[email protected]',   // Product Support (all four)
758    // Partnership Opportunity → the head of the selected division:
759    divisions: {
760      'union-electronics': '[email protected],[email protected]',  // Electronics (both)
761      'aluna':             '[email protected]',
762      'fmcg':              '[email protected]',
763      'labels':            '[email protected]',
764      'union-services':    '[email protected],[email protected],[email protected],[email protected]',
765      'commercial':        '[email protected]'   // Commercial AC & Appliance Projects
766    },
767    fallback: '[email protected]'                  // partnership with no division picked, etc.
768  };
769  function routeTo(intent, divisionKey) {
770    if (intent === 'partnership') return MAIL_ROUTES.divisions[divisionKey] || MAIL_ROUTES.fallback;
771    if (intent === 'support') return MAIL_ROUTES.support;
772    return MAIL_ROUTES[intent] || MAIL_ROUTES.fallback;
773  }
774
775  // EmailJS — client-side email that works on ANY hosting (GitHub Pages, a cloud
776  // server, anywhere): the visitor's browser sends the enquiry through YOUR connected
777  // mailbox. Fill the 3 IDs from the EmailJS dashboard (emailjs.com → Account) to turn
778  // it on; while any is blank the form uses the Web3Forms/mailto path below, so nothing
779  // breaks.
780  //   SENT FROM: connect [email protected] as the EmailJS Email Service → that becomes
781  //              the From address on every enquiry.
782  //   SENT TO:   the template's "To" field = {{to_email}} (dynamic) — the recipient is
783  //              supplied by the code (routeTo → MAIL_ROUTES; all routes = [email protected]
784  //              during testing), so we can retarget it without editing the template. NOTE: a public,
785  //              dynamic {{to_email}} can in theory be abused as an open relay — fine for
786  //              this testing phase; before the corporate launch, lock it down (fixed To,
787  //              or move sending to a backend/Azure Function).
788  //   reply_to is set to the visitor's email, so replies go straight back to the customer.
789  // Remember to add this site's domain under EmailJS → Account → Security → Allowed origins.
790  const EMAILJS = { publicKey: 'LN88tTw5xPwrUuH67', serviceId: 'service_87ki86s', templateId: 'template_zun7cpc' };
791  let _emailjsReady = null;
792  function ensureEmailJS() {
793    if (_emailjsReady) return _emailjsReady;
794    _emailjsReady = new Promise((res, rej) => {
795      if (window.emailjs) { try { emailjs.init({ publicKey: EMAILJS.publicKey }); } catch (e) {} return res(); }
796      const s = document.createElement('script');
797      s.src = 'emailjs.min.js';  // self-hosted (bundled) — no external CDN dependency
798      s.onload = () => { try { emailjs.init({ publicKey: EMAILJS.publicKey }); } catch (e) {} res(); };
799      s.onerror = rej;
800      document.head.appendChild(s);
801    });
802    return _emailjsReady;
803  }
804
805  el('contactForm').addEventListener('submit', e => {
806    e.preventDefault();
807    const sel = el('cfIntent');
808    const opt = sel.options[sel.selectedIndex];
809    const divKey = el('cfDivision') ? el('cfDivision').value : '';
810    const to = routeTo(sel.value, divKey);   // central send → routed by enquiry type / division
811    const name = (el('cfName').value || '').trim();
812    const email = (el('cfEmail').value || '').trim();
813    const phone = (el('cfPhone').value || '').trim();
814    const subjIn = (el('cfSubject').value || '').trim();
815    const msg = (el('cfMessage').value || '').trim();
816    const isPartner = sel.value === 'partnership';
817    const subject = (subjIn || (opt ? opt.text : 'Website enquiry')) + (name ? ' — ' + name : '');
818    let division = '', partType = '', org = '', other = '';
819    const lines = [
820      'Enquiry type: ' + (opt ? opt.text : ''),
821      'Name: ' + name,
822      'Email: ' + email,
823      'Phone: ' + phone
824    ];
825    if (isPartner) {
826      const dvSel = el('cfDivision');
827      division = dvSel && dvSel.value ? ((dvSel.options[dvSel.selectedIndex] || {}).text || '') : '';
828      partType = el('cfPartType') ? el('cfPartType').value : '';
829      org = el('cfOrg') ? (el('cfOrg').value || '').trim() : '';
830      other = el('cfOther') ? (el('cfOther').value || '').trim() : '';
831      if (division) lines.push('Division: ' + division);
832      lines.push('Partnership type: ' + partType);
833      lines.push('Organisation: ' + org);
834      lines.push('Other information: ' + other);
835    }
836    lines.push('', msg);
837
838    const btn = el('submitBtn');
839    const setBtn = t => { btn.childNodes[0].nodeValue = t; };
840    // Nothing goes out half-filled: a name, a working email, a phone number and a
841    // message are all needed, and a partnership enquiry has to say which division it
842    // is about — without that it lands in the fallback inbox and is forwarded by hand.
843    var EMAIL_OK = new RegExp('^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$');
844    var DIGITS = function (v) { return String(v).replace(new RegExp('[^0-9]', 'g'), ''); };
845    var flag = function (field, label) {
846      if (field) {
847        field.style.borderColor = '#c0392b';
848        field.focus();
849        var clear = function () { field.style.borderColor = ''; };
850        field.addEventListener('input', clear, { once: true });
851        field.addEventListener('change', clear, { once: true });
852      }
853      setBtn(label);
854      setTimeout(function () { setBtn('SEND MESSAGE'); }, 3000);
855    };
856    // A reply needs somewhere to go and someone to reply to, so these are asked for
857    // as well. The email pattern is built from a string rather than written as a
858    // regular expression, so nothing can be lost when this file is generated.
859    var EMAIL_OK = new RegExp('^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$');
860    var DIGITS = function (v) { return String(v).replace(new RegExp('[^0-9]', 'g'), ''); };
861    if (!name) { flag(el('cfName'), 'ADD YOUR NAME '); return; }
862    if (!email) { flag(el('cfEmail'), 'ADD YOUR EMAIL '); return; }
863    if (!EMAIL_OK.test(email)) { flag(el('cfEmail'), 'CHECK YOUR EMAIL '); return; }
864    if (!phone || DIGITS(phone).length < 6) { flag(el('cfPhone'), 'ADD YOUR PHONE NUMBER '); return; }
865    if (!msg) { flag(el('cfMessage'), 'WRITE A MESSAGE '); return; }
866    if (isPartner && !divKey) { flag(el('cfDivision'), 'CHOOSE A DIVISION '); return; }
867    const openMail = label => {
868      window.location.href = 'mailto:' + to + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(lines.join('\n'));
869      setBtn(label || 'OPENING EMAIL… ');
870      setTimeout(() => setBtn('SEND MESSAGE'), 4000);
871    };
872
873    // Preferred: EmailJS (client-side; works on static or cloud hosting). Sends the
874    // enquiry through your connected mailbox, routed to the selected option's inbox.
875    if (EMAILJS.publicKey && EMAILJS.serviceId && EMAILJS.templateId) {
876      if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { setBtn('CHECK YOUR EMAIL '); setTimeout(() => setBtn('SEND MESSAGE'), 3000); return; }
877      btn.disabled = true; setBtn('SENDING… ');
878      // Two message parts for one template: partner_details renders inside a BOLD element in the
879      // template (blank for general enquiries → nothing shows), message_text is the visitor's message
880      // in normal weight. EmailJS escapes HTML inside variables, so the bold styling lives in the template.
881      var partnerDetails = isPartner
882        ? ('Division: ' + division + '\nPartnership type: ' + partType + '\nOrganisation: ' + org + '\nOther information: ' + other + '\n\n')
883        : '';
884      ensureEmailJS()
885        .then(() => emailjs.send(EMAILJS.serviceId, EMAILJS.templateId, {
886          to_email: to, subject: subject, from_name: name || 'Website visitor', reply_to: email,
887          name: name, email: email, phone: phone,
888          enquiry_type: opt ? opt.text : '',
889          division: division, partnership_type: partType, organisation: org, other_information: other,
890          partner_details: partnerDetails,   // bold block in the template (blank for general enquiries)
891          message_text: msg,                 // the visitor's message (normal weight)
892          message: lines.join('\n')          // full plain-text block (fallback)
893        }))
894        .then(() => {
895          setBtn('MESSAGE SENT ✓ ');
896          el('contactForm').reset(); syncPartnership();
897          showFormModal();                                     // "your request has been submitted" popup
898          setTimeout(() => { setBtn('SEND MESSAGE'); btn.disabled = false; }, 5000);
899        })
900        .catch(() => { btn.disabled = false; openMail('OPENING EMAIL… '); }); // fallback so the enquiry isn't lost
901      return;
902    }
903
904    // No form service configured yet → open the visitor's email app.
905    if (!WEB3FORMS_KEY) { openMail(); return; }
906
907    // Basic validation before a real send.
908    if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { setBtn('CHECK YOUR EMAIL '); setTimeout(() => setBtn('SEND MESSAGE'), 3000); return; }
909
910    // Send through the website (no page reload; lands in the inbox tied to the key).
911    btn.disabled = true; setBtn('SENDING… ');
912    fetch('https://api.web3forms.com/submit', {
913      method: 'POST',
914      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
915      body: JSON.stringify({
916        access_key: WEB3FORMS_KEY,
917        subject: subject,
918        from_name: name || 'Website visitor',
919        name: name, email: email, phone: phone,
920        enquiry_type: opt ? opt.text : '',
921        division: division, partnership_type: partType, organisation: org, other_information: other,
922        message: lines.join('\n'),
923        botcheck: ''
924      })
925    })
926      .then(r => r.json())
927      .then(d => {
928        if (d && d.success) {
929          setBtn('MESSAGE SENT ✓ ');
930          el('contactForm').reset(); syncPartnership();
931          showFormModal();                                     // "your request has been submitted" popup
932          setTimeout(() => { setBtn('SEND MESSAGE'); btn.disabled = false; }, 5000);
933        } else { throw new Error('service'); }
934      })
935      .catch(() => { btn.disabled = false; openMail('OPENING EMAIL… '); }); // fallback so the enquiry isn't lost
936  });
937
938  // Contact "request submitted" popup — shown after the website sends the enquiry.
939  function showFormModal() { var m = el('formModal'); if (m) m.classList.add('open'); }
940  (function () {
941    var m = document.getElementById('formModal'); if (!m) return;
942    var close = function () { m.classList.remove('open'); };
943    m.addEventListener('click', function (e) { if (e.target.closest('[data-fm]')) close(); });
944    document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
945  })();
946
947  /* ---------- KPI count-up ---------- */
948  function initKpis() {
949    const els = Array.from(document.querySelectorAll('[data-kpi]'));
950    if (!els.length) return;
951    let done = false;
952    els.forEach(e => { const raw = e.getAttribute('data-kpi'); e.__t = parseInt(raw.replace(/[^0-9]/g, ''), 10) || 0; e.__s = raw.replace(/[0-9]/g, ''); e.textContent = '0' + e.__s; });
953    const animate = () => {
954      const dur = 1600, start = performance.now();
955      const tick = now => { const t = Math.min(1, (now - start) / dur); const e2 = 1 - Math.pow(1 - t, 3); els.forEach(e => e.textContent = Math.round(e.__t * e2) + e.__s); if (t < 1) requestAnimationFrame(tick); };
956      requestAnimationFrame(tick);
957    };
958    const obs = new IntersectionObserver(ents => { ents.forEach(en => { if (en.isIntersecting && !done) { done = true; animate(); obs.disconnect(); } }); }, { threshold: 0.4 });
959    els.forEach(e => obs.observe(e));
960  }
961
962  /* ---------- milestones 3D drum ---------- */
963  function initMilestones() {
964    const sec = document.querySelector('[data-ms-section]');
965    if (!sec) return;
966    const panels = Array.from(document.querySelectorAll('[data-ms-panel]'));
967    const counter = document.querySelector('[data-ms-counter]');
968    const rail = document.querySelector('[data-ms-rail]');
969    const progdot = document.querySelector('[data-ms-progdot]');
970    const prevBtn = document.querySelector('[data-ms-prev]');
971    const nextBtn = document.querySelector('[data-ms-next]');
972    const N = panels.length;
973    if (!N) return;
974    const pad = n => (n < 10 ? '0' + n : '' + n);
975    const STEP = 60, FACTOR = 0.5 / Math.tan((STEP / 2) * Math.PI / 180);
976    const measure = () => panels.forEach(p => { const vp = p.closest('[data-ms-viewport]'); p.__r = (vp ? vp.offsetHeight : 400) * FACTOR; });
977    measure();
978    let target = 0, cur = 0, lastActive = -1, running = false, prog = 0;
979    panels.forEach(p => { p.__dim = p.querySelector('[data-ms-dim]'); p.__ring = p.querySelector('[data-ms-ring]'); });
980    const setActive = active => {
981      panels.forEach((p, i) => { const on = i === active; if (p.__dim) p.__dim.style.opacity = on ? '0' : '0.5'; if (p.__ring) p.__ring.style.opacity = on ? '1' : '0'; });
982      if (counter) counter.innerHTML = pad(active + 1) + ' <span style="color:#5a5a5a">/ ' + pad(N) + '</span>';
983    };
984    const render = raw => {
985      panels.forEach((p, i) => {
986        const ang = (i - raw) * STEP;
987        const vis = Math.abs(ang) < 91;
988        const op = vis ? Math.max(0, Math.pow(Math.cos(ang * Math.PI / 180), 0.9)) : 0;
989        const r = p.__r || 300;
990        p.style.transform = 'translateZ(' + (-r).toFixed(0) + 'px) rotateX(' + (-ang).toFixed(2) + 'deg) translateZ(' + r.toFixed(0) + 'px)';
991        p.style.opacity = op.toFixed(3);
992        p.style.zIndex = vis ? String(100 - Math.round(Math.abs(ang))) : '0';
993      });
994      if (rail && progdot) progdot.style.top = (Math.min(1, Math.max(0, raw / (N - 1))) * (rail.offsetHeight - 7)).toFixed(1) + 'px';
995      const active = Math.min(N - 1, Math.round(raw));
996      if (active !== lastActive) { lastActive = active; setActive(active); }
997    };
998    const tick = () => { cur += (target - cur) * 0.12; if (Math.abs(target - cur) < 0.0006) { cur = target; render(cur); running = false; return; } render(cur); requestAnimationFrame(tick); };
999    const kick = () => { if (!running) { running = true; requestAnimationFrame(tick); } };
1000    const go = n => { prog = Math.max(0, Math.min(N - 1, n)); target = prog; lastWheel = Date.now(); kick(); };
1001    const vp = document.querySelector('[data-ms-viewport]');
1002    const headerEl = document.querySelector('header');
1003    let engaged = false, lastWheel = 0;
1004    const SENS = 520;
1005    const onWheel = e => {
1006      const rect = sec.getBoundingClientRect(), vh = window.innerHeight;
1007      const hdr = headerEl ? headerEl.offsetHeight : 74;
1008      // take over the wheel only once the section is snapped fully into place (its top resting at the
1009      // sticky header). scroll-snap-stop:always lands it there without overshoot, so the barrel never
1010      // fights the snap. Any partial/half-scrolled state -> the page scrolls (and snaps) normally.
1011      const cap = Math.max(10, vh * 0.03);
1012      const near = rect.top >= -cap && rect.top <= hdr + cap;
1013      const dir = e.deltaY > 0 ? 1 : -1;
1014      const atEnd = (dir > 0 && prog >= N - 1 - 0.001) || (dir < 0 && prog <= 0.001);
1015      if (!near || atEnd) { engaged = false; return; }
1016      e.preventDefault();
1017      engaged = true;
1018      prog = Math.max(0, Math.min(N - 1, prog + e.deltaY / SENS));
1019      target = prog; lastWheel = Date.now(); kick();
1020    };
1021    if (vp) vp.addEventListener('wheel', onWheel, { passive: false });
1022    if (prevBtn) prevBtn.onclick = () => go(Math.round(prog) - 1);
1023    if (nextBtn) nextBtn.onclick = () => go(Math.round(prog) + 1);
1024    window.addEventListener('resize', () => { measure(); render(cur); });
1025    /* Auto-advance the drum, but only in the desktop layout AND while the section is
1026       actually on-screen. The timer is cleared entirely off-screen / when the tab is
1027       hidden / on mobile (where the swipe carousel takes over), so nothing animates in
1028       the background. */
1029    let drumTimer = 0, drumOn = false;
1030    const drumBeat = () => {
1031      if (window.innerWidth <= 900) return;
1032      const rect = sec.getBoundingClientRect(), vh = window.innerHeight;
1033      const covers = rect.top <= vh * 0.5 && rect.bottom >= vh * 0.5;
1034      if (covers && Date.now() - lastWheel > 3200) go((Math.round(prog) + 1) % N);
1035    };
1036    const startDrum = () => { if (!drumTimer && window.innerWidth > 900) drumTimer = setInterval(drumBeat, 3400); };
1037    const stopDrum = () => { if (drumTimer) { clearInterval(drumTimer); drumTimer = 0; } };
1038    const drumIO = new IntersectionObserver(es => es.forEach(e => {
1039      drumOn = e.isIntersecting && e.intersectionRatio >= 0.45;
1040      if (drumOn) startDrum(); else stopDrum();
1041    }), { threshold: [0, 0.45, 1] });
1042    drumIO.observe(sec);
1043    document.addEventListener('visibilitychange', () => { if (document.hidden) stopDrum(); else if (drumOn) startDrum(); });
1044    cur = target; render(cur);
1045  }
1046
1047  /* ---------- fit hero to viewport minus header ---------- */
1048  function fitHero() { const h = document.querySelector('header'); if (h) document.documentElement.style.setProperty('--hdr', h.offsetHeight + 'px'); }
1049  window.addEventListener('resize', fitHero);
1050
1051  /* ---------- init ---------- */
1052  setHero(0);
1053  setDiv(0);
1054  setPartner(0);
1055  fitHero();
1056  setTimeout(fitHero, 300);
1057  setTimeout(layoutAll, 60);
1058  setTimeout(layoutAll, 400);
1059  setTimeout(observeSlots, 80);
1060  setTimeout(() => { initMilestones(); initKpis(); }, 150);
1061  window.addEventListener('load', () => { layoutAll(); fitHero(); });
1062
1063})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.